/*
(c) 2011 Mikle[Subterranean Devil]
Kbt framework (runtime support + compiler)
Requires: javascript 1.7+ (currently, Firefox 2+ only)
*/

/// Kbt namespace
var Kbt = { version: "1.0" };

//
// lib part
//

/// Formats a string by replacing placeholders of kind %name% with
/// values specified in formatHash. %% stands for single %. Example:
/// Kbt.format ("line %x% is %d%%%", { x: "gg", d: 100 }) == "line gg is 100%"
/// @tparam String Format string
/// @tparam Object Hash of values, keyed by placeholder name. Entries not
/// present here but specified in placeholders of the format string will be
/// present as 'undefined' in the resulting string
Kbt.format = kbtFormat = function (formatString,formatHash)
{
	return formatString.toString ().replace (/%([^%]*)%/g,function (str,ph)
	{ return ph==""? "%" : formatHash[ph]; });
}

/// Shortcut for document.write (Kbt.format (formatString,formatHash))
document.writef = function (formatString,formatHash)
{
	document.write (Kbt.format (formatString,formatHash));
}

/// Replaces HTML special characters with corresponding entities:
/// & => &amp; " => &quot; < => &lt; > => &gt;
Kbt.htmlSpecialChars = function (str)
{
	return str.replace (/&/g,"&amp;")
		.replace (/"/g,"&quot;")
		.replace (/</g,"&lt;")
		.replace (/>/g,"&gt;");
}

/// Throw alert and Error exception with given msg. Typical usage can be like
/// this: var someObj = getObj () || Kbt.panic ("object is null or undefined!");
/// @tparam msg String Message to print and throw
Kbt.panic = kbtPanic = function (msg)
{
	alert (msg);
	throw Error (msg);
}

/// Check a condition and Kbt.panic with given message if it is false
/// @tparam flag Bool Condition to check
/// @tparam msg String Message to panic with
Kbt.assert = kbtAssert = function (flag,msg)
{
	if (!flag) Kbt.panic (msg);
}

Kbt.Func = KbtFunc = {};

/// Return a delegate - function func bound to obj. The delegate is a function,
/// calling it will forward the call to func with 'this' always set to obj,
/// no matter what activation context it is called in.
/// Object returned also has two additional members set: unbound - the function
/// unbound (namely, the 'func'), and boundTo - ref to object the function is
/// bound to. Kbt.bind itself uses these members to determine if func is already
/// bound and to optimize by returning it instead of binding it second time
/// (which would have no useful effect).
/// Kbt framework provides a shortcut: #delegate { code } or
/// #delegate (params) { code }, which stand for:
/// Kbt.bind (this,function (params) { code })
Kbt.bind = kbtBind = function (obj,func)
{
	function bound () { return func.apply (obj,arguments); }
	bound._Kbt$bound = true;
	bound.unbound = func;
	bound.boundTo = obj;
	// kind of optimization: if a function is already bound, it's no sense
	// to bind it again
	return func._Kbt$bound? func : bound;
};

/// Copy enumerable properties from obj2 to ones in obj1 with same names,
/// return obj1 (not a copy). Existing properties with same name in obj1 will
/// be overwritten.
/// @param obj1 Object to be patched
/// @param obj2 Object containing the patch
/// @return obj1 with added/replaced properties from obj2. It's not a shallow
/// copy of obj1, it's obj1 itself.
Kbt.patchObject = kbtPatchObject = function (obj1,obj2)
{
	for (var i in obj2) obj1[i] = obj2[i];
	return obj1;
};

/// Returns a copy of object obj. If obj implements MxCopyable, uses its copy
/// method to make the copy. Otherwise, returns shallow copy, with all members
/// that implement MxCopyable copied via their copy methods, and all other
/// members copied by assignment/reference.
/// @param obj Object to copy
/// @return obj's copy
Kbt.copyObject = kbtCopyObject = function (obj)
{
	var asCopyable = MxCopyable.dynamicCast (obj);
	if (asCopyable) return asCopyable.copy (obj);
	else
	{
		var result = {};
		for (var i in obj)
		{
			var member = obj[i];
			asCopyable = MxCopyable.dynamicCast (member);
			result[i] = (asCopyable && asCopyable.copy ()) || member;
		}
		return result;
	}
};

/// Invokes a sequence of methods with given names for a specified object. The
/// limitation is, all of these methods must take exactly one parameter, and
/// neither of them should not return a value of essential lifetime.
/// Convenient to invoke multiple setters for a single object.
/// @param obj Object to invoke methods for
/// @param toApply Hash of methods to invoke. Keys are method names, values are
/// parameters to pass to corresponding methods.
/// @return obj, so that the call could be chained
Kbt.multiApply = kbtMultiApply = function (obj,toApply)
{
	for (var k in toApply)
		obj[k] (toApply[k]);
	return obj;
};

KbtFunc.placeholder = function ()
{
	throw new Error ("Kbt.Func.placeholder - unimplemented placeholder");
};

KbtFunc.virtual = function (func)
{
	return { _Kbt$func: func, _Kbt$virtual: true };
};

function _kbt$pureVirtual ()
{
	throw new Error("Kbt.Func.virtual - pure virtual called");
};


//
// mixin main support
//

Kbt.Mixin = KbtMixin = {}; // mixins namespace
// mxId => mixin
KbtMixin._Kbt$regMixins = [];
// mtdId => { mx: mixin, mtdName: name:string, isVirtual: bool }
KbtMixin._Kbt$regMethods = [];
// mtdName => mxId => mtdId
KbtMixin._Kbt$revRegMethods = {};

/// Enable mixin usage interface for object obj
KbtMixin.enable = function (obj)
{
	if (obj._Kbt$mxRoot==undefined)
	{
		obj._Kbt$mxRoot = obj;
		obj._Kbt$methods = {}; // mtdId => bound function
		obj._Kbt$gateways = {}; // mxId => { _Kbt$mxRoot: obj, [mtdName: bound func] }
		// also serves as a flag that mxId is enabled
		KbtMixin._Kbt$mxEnable (obj,MxBase);
	}
};

KbtMixin._Kbt$mxEnable = function (obj,pMxRef)
{
	kbtAssert (pMxRef._Kbt$mxId!=undefined,
		"MxBase::mxEnable (pMxRef): pMxRef is not a mixin");
	if (!obj._Kbt$gateways[pMxRef._Kbt$mxId])
	{
		// obj does not have pMxRef enabled yet

		// do a trick: obj will be new pMxRef instance's prototype. It will
		// allow us to set up method shortcuts in obj only but still have
		// them visible from this pMxRef instance
		var prevProto = pMxRef.prototype;
		pMxRef.prototype = obj;
		try {
			result = new pMxRef ();
			return result;
		}
		finally // as ctor has done its job, return prev prototype back
		{ pMxRef.prototype = prevProto; }
	}
	else
		return obj;
};

var MxBase = undefined; // temporary, will be replaced as MxBase is defined

// helper for mixin instance constructor/overrider
function _kbt$applyDefinitions (methods,defines)
{
	var mxRoot = this._Kbt$mxRoot;
	for (var i in defines)
	{
		let define = defines[i],
			mtd = KbtMixin._Kbt$regMethods[i],
			mtdName = mtd.mtdName,
			prevDef = mxRoot[mtdName];
		let newMtd;
		if (define===KbtFunc.placeholder)
		{
			// placeholder encountered
			let me = this,mtdId = i;
			newMtd = {
				implement: function (func)
				{
					var prevMtd = mxRoot[mtdName];
					var newMtd = mxRoot._Kbt$methods[mtdId] =
						kbtPatchObject (kbtBind (me,func));
					// shortcut is ours - update shortcut
					if (prevMtd==undefined || prevMtd._Kbt$methodOf==mtd.mx)
						mxRoot[mtdName] = newMtd;
				},
				_Kbt$methodOf: mtd.mx
			};
		}
		else
			newMtd = kbtBind (this,define);
		mxRoot._Kbt$methods[i] = newMtd;

		// add named shortcut
		if (prevDef==undefined || prevDef._Kbt$methodOf===undefined)
		{
			// no shortcut or existing shortcut is unrelated - define
			mxRoot[mtdName] = kbtPatchObject
				(newMtd,{ _Kbt$methodOf: mtd.mx });
		}
		else if (prevDef._Kbt$methodOf===mtd.mx)
		{
			// our definition is an override and there is no ambiguity yet
			mxRoot[mtdName] = kbtPatchObject
				(newMtd,{ _Kbt$methodOf: mtd.mx });
		}
		else if (prevDef._Kbt$methodOf!=false)
		{
			// our definition is not an override and introduces first
			// ambiguity
			var
				pUsing = function (pMxRef)
				{
					kbtAssert (pMxRef._Kbt$mxId!=undefined,
						"using/resolveTo (pMxRef): pMxRef is not a mixin");
					var mtdId = KbtMixin
						._Kbt$revRegMethods[mtdName][pMxRef._Kbt$mxId];
					kbtAssert (mtdId!=undefined,
						"using/resolveTo (pMxRef): method '"+mtdName+"' is not implemented in mixin #"+pMxRef._Kbt$mxId);
					return mxRoot._Kbt$methods[mtdId];
				},
				pResolveTo = function (pMxRef) // disambiguator
				{
					var func = mxRoot[mtdName].using (pMxRef);
					mxRoot[mtdName] = kbtPatchObject(
						function () { return func.apply (this,arguments) },
						// it's next argument, don't get mixed
						{
							using: pUsing,
							resolveTo: pResolveTo,
							_Kbt$methodOf: pMxRef
						});
				};
			mxRoot[mtdName] = kbtPatchObject (
			function ()
			{
				Kbt.panic ("Kbt.Mixin - ambiguous method shortcut '"+mtdName+"', use .using or .resolveTo to disambiguate");
			},
			{
				_Kbt$methodOf: false, // ambiguity marker
				/// Dynamic desambiguator. Allows to pick the implementation
				/// from specific mixin.
				using: pUsing,
				/// Freezing static desambiguator. Once you call it, this
				/// named shortcut will be bound to implementation of pMxRef.
				/// You can still use using to access other implementations
				/// or call resolveTo to readjust.
				resolveTo: pResolveTo
			});
		}
	}
}

/// Declare a mixin.
/// @return The mixin declared. Consider the object returned as a class.
KbtMixin.declare = function (enablesList,addsList,ctor)
{
	// mxId: int, identifier of the mixin in Kbt.Mixin registry
	var mxId = KbtMixin._Kbt$regMixins.length;

	// result (as ctor function)
	function mixin ()
	{
		KbtMixin.enable (this);
		var mxRoot = this._Kbt$mxRoot;
		kbtAssert (!mxRoot._Kbt$mxFrozen,"MxBase::mxEnable - can't mixin after MxBase::mxFreeze");

		// first, apply mixins from enablesList
		for (var i in enablesList)
		{
			var pMx = enablesList[i],
				pOvList;
			if (mxRoot._Kbt$gateways[pMx._Kbt$mxId])
				continue; // the mixin is already enabled
			if (pOvList = pMx.overrides)
				pMx = KbtMixin._Kbt$regMixins[pMx._Kbt$mxId];
			pMx.apply (mxRoot);
		}

		// add/redefine our own definitions/overrides
		_kbt$applyDefinitions.call (this,this._Kbt$methods,mixin._Kbt$defines);

		// apply constructor
		if (ctor!=undefined) ctor.apply (this);

		// officially acknowledge us mixed in
		mxRoot._Kbt$gateways[mixin._Kbt$mxId] = {};
		delete i;
		// end of mixin instance constructor
	}
	// fill in some extras
	mixin._Kbt$mxId = mxId;
	mixin._Kbt$ctor = ctor;
	mixin._Kbt$ownMethodsByName = {};
	mixin._Kbt$enablesMethod = function (mtdName)
	{
		if (this._Kbt$ownMethodsByName[mtdName]!=undefined)
			return this._Kbt$ownMethodsByName[mtdName];
		else
		{
			var enablesMx = this._Kbt$enables;
			for (var i in enablesMx)
			{
				var mtdId = enablesMx[i]._Kbt$enablesMethod (mtdName);
				if (mtdId!=undefined) return mtdId;
			}
			return undefined;
		}
	};

	// register the mixin
	KbtMixin._Kbt$regMixins[mxId] = mixin;

	// every mixin implicitly enables MxBase (except of MxBase itself which
	// defines itself explicitly)
	if (MxBase) enablesList.unshift (MxBase);

	// register added methods and add them to definitions:
	mixin._Kbt$defines = {}; // methods we define (our own) or redefine
	mixin._Kbt$ownMethods = [];
	// own methods
	for (let mtdName in addsList)
	{
		let mtdId = KbtMixin._Kbt$regMethods.length,
			mtd = addsList[mtdName];
		let isVirtual = false;
		if (mtd===KbtFunc.virtual)
		{
			// declaration 'mtdName: Kbt.Func.virtual'
			mtd = {
				_Kbt$func: _kbt$pureVirtual,
				_Kbt$virtual: true
			};
		}
		else if (mtd instanceof Function)
		{
			// declaration 'mtdName: function () ...'
			// or 'mtdName: Kbt.Func.placeholder
			mtd = { _Kbt$func: mtd, _Kbt$virtual: false };
		}
		else
			kbtAssert (mtd._Kbt$func!=undefined,
				"Kbt.Mixin.declare: only functions or virtual function placeholders can be in to-add list");
		// register method
		KbtMixin._Kbt$regMethods[mtdId] =
		{
			mx: mixin,
			mtdName: mtdName,
			isVirtual: mtd._Kbt$virtual
		};
		let revMtdReg = KbtMixin._Kbt$revRegMethods[mtdName]
			|| (KbtMixin._Kbt$revRegMethods[mtdName] = {});
		revMtdReg[mxId] = mtdId;
		mixin._Kbt$defines[mtdId] = mtd._Kbt$func;
		mixin._Kbt$ownMethods.push (mtdId);
		mixin._Kbt$ownMethodsByName[mtdName] = mtdId;
	}

	// mxEnables[mxId][name] = override_type
	var enables = mixin._Kbt$enables = [],
		alreadyEnabled = {}; // mxId => true, initially add self
	alreadyEnabled[mxId] = true; // initially add self (just for precaution)
	//document.write (htmlize (enablesList.toString ()));
	for (let i in enablesList)
	{
		let pMx = enablesList[i],
			pOvList;
		kbtAssert (pMx!=undefined && pMx._Kbt$mxId!=undefined,
			"Kbt.Mixin.declare (enablesList,addsList,ctor): not a mixin in enablesList");
		if (pOvList = pMx.overrides)
			pMx = KbtMixin._Kbt$regMixins[pMx._Kbt$mxId];
		kbtAssert (!alreadyEnabled[pMx._Kbt$mxId],
			"Kbt.Mixin.declare (enablesList,addsList,ctor): duplicate mixin #"+pMx._Kbt$mxId+" in enablesList");
		alreadyEnabled[pMx._Kbt$mxId] = true;
		enables.push (pMx);

		// add entries from pOvList to our defines
		if (pOvList)
		for (let mtdName in pOvList)
		{
			let mtdId = pMx._Kbt$enablesMethod (mtdName);
			kbtAssert (mtdId!=undefined,
				"Kbt.Mixin.declare (enablesList,addsList,ctor): method '"+mtdName+"' is not defined in mixin #"+pMx._Kbt$mxId+" from enableList and can't be overridden");
			kbtAssert (KbtMixin._Kbt$regMethods[mtdId].isVirtual,
				"Kbt.Mixin.declare (enablesList,addsList,ctor): method '"+mtdName+"' is not virtual in mixin #"+pMx._Kbt$mxId+" from enableList and can't be overridden");
			let pOv = pOvList[mtdName];
			mixin._Kbt$defines[mtdId] = pOv;
		}
	}

	delete alreadyEnabled;
	delete enables;

	// 'static' mixin methods:

	/// Prepare a list of methods to override. Use this call
	/// for filling to-enable list in Kbt.Mixin.declare.
	mixin.override = function (overrideList)
	{
		var result = {};
		result._Kbt$mxId = this._Kbt$mxId;
		result.overrides = overrideList; // this is also an indicator
		// that it's not the mixin itself, just its id + override list
		return result;
	};

	mixin._Kbt$identifyOverrideList = function (obj,missing)
	{
		// does the same as identifyOverrideList (see below) but in addition
		// tells if there are entries that are required but not implemented
		// in obj (fills _Kbt$incomplImpl flag) if missing is true

		// build virtual functions list by name
		var pVirtIds = {},
			pCompIds = {},
			pOptIds = {},
			reg = KbtMixin._Kbt$regMethods;
		function buildMe (pMx)
		{
			var enables = pMx._Kbt$enables,
				defines = pMx._Kbt$defines;
			// build bottom-up, so that bottommost overrides would take place
			for (var i in enables) buildMe (enables[i]);
			for (var i in defines)
			{
				var regMtd = reg[i];
				if (regMtd.isVirtual)
				if (defines[i]==_kbt$pureVirtual)
					pCompIds[regMtd.mtdName] = i;
				else
					pOptIds[regMtd.mtdName] = i;
			}
		}
		buildMe (this);
		// fill compulsory ids
		if (missing)
		{
			pVirtIds._Kbt$incomplImpl = false;
			for (var mtdName in pCompIds)
			{
				var func = obj[mtdName];
				if (func instanceof Function)
					// object has the implementation
					pVirtIds[mtdName] = kbtBind (obj,func);
				else
				{
					pVirtIds._Kbt$incomplImpl = true;
					return pVirtIds;
				}
			}
		}
		else
		for (var mtdName in pCompIds)
		{
			var func = obj[mtdName];
			if (func instanceof Function)
				// object has the implementation
				pVirtIds[mtdName] = kbtBind (obj,func);
			else
				pVirtIds[mtdName] = true;
		}
		// fill optional ids
		for (var mtdName in pOptIds)
		{
			var func = obj[mtdName];
			if (func instanceof Function)
				// object has the implementation
				pVirtIds[mtdName] = kbtBind (obj,func);
			else
				delete pVirtIds[mtdName];
		}
		return pVirtIds;
	}

	/// Identify possible implementation of this mixin in obj (obj has
	/// members of same names as virtual or pure virtual functions of this
	/// mixin, and they are functions) and return override list that can
	/// be used to override implementation of this mixin (via
	/// MxBase::mxOverride) with forwarding to implementation in obj
	mixin.identifyOverrideList = function (obj)
	{
		return this._Kbt$identifyOverrideList (obj,false);
	};

	/// Return gateway to this mixin implemented in obj if any, or return
	/// undefined if the mixin is not implemented in obj
	mixin.dynamicCast = function (obj)
	{
		if (!obj) return null; // obvious case 
		var mxRoot = obj._Kbt$mxRoot,
			gateway = mxRoot && mxRoot._Kbt$gateways[mixin._Kbt$mxId];
		if (!(mxRoot&&gateway)) return undefined;
		if (!gateway._Kbt$mxRoot)
		{
			// gateway is not yet filled
			gateway._Kbt$mxRoot = mxRoot;
			var bp = mixin._Kbt$gatewayBlueprint;
			for (var mtdName in bp)
			{
				var mtds = bp[mtdName];
				if (mtds.length>1)
				{
					// ambiguity
					var pUsing = function (pMxRef)
					{
						kbtAssert (pMxRef._Kbt$mxId!=undefined,
							"using/resolveTo (pMxRef): pMxRef is not a mixin");
						var mtdId = KbtMixin
							._Kbt$revRegMethods[mtdName][pMxRef._Kbt$mxId];
						kbtAssert (mtdId!=undefined,
							"using/resolveTo (pMxRef): method '"+mtdName+"' is not implemented in mixin #"+pMxRef._Kbt$mxId);
						return mxRoot._Kbt$methods[mtdId];
					},
					pResolveTo = function (pMxRef) // disambiguator
					{
						var func = mxRoot[mtdName].using (pMxRef);
						mxRoot[mtdName] = kbtPatchObject(
							function () { return func.apply (this,arguments) },
							// it's next argument, don't get mixed
							{
								using: pUsing,
								resolveTo: pResolveTo
							});
					};
					gateway[mtdName] = kbtPatchObj (
						function ()
						{
							Kbt.panic ("Kbt.Mixin - ambiguous method shortcut '"+mtdName+"', use .using or .resolveTo to disambiguate");
						},
						{
							using: pUsing,
							resolveTo: pResolveTo
						});
				}
				else
					gateway[mtdName] = mxRoot._Kbt$methods[mtds[0]];
			}
		}
		return gateway;
	};

	var mtdBP = mixin._Kbt$gatewayBlueprint = []; // name => [mtdId]
	var mtdUsed = {};
	function fillGatewayBlueprint (pMx)
	{
		var enables = pMx._Kbt$enables,
			defines = pMx._Kbt$defines;
		for (var i in enables) fillGatewayBlueprint (enables[i]);
		for (var i in defines)
		if (!mtdUsed[i])
		{
			mtdUsed[i] = true;
			var regName = KbtMixin._Kbt$regMethods[i].mtdName,
				list = mtdBP[regName] || (mtdBP[regName] = []);
			list.push (i);
		}
	}
	fillGatewayBlueprint (mixin);

	// finally, clean up and return what we have
	delete mxId;
	delete mtdUsed;
	delete mtdBP;
	return mixin;
};

//
// and some predefined mixins
//

/// MxBase
/// Provides basic interface for a mixin: enabling it in an object, overriding
/// virtual functons, etc. MxBase is implicitly enabled by any other mixin.
Kbt.MxBase = MxBase = KbtMixin.declare ([],
// adds
{
	/// pMxRef: Mixin
	/// If mixin pMxRef is not present in this object, construct and implant it
	/// @return gateway to pMxRef
	mxEnable: function (pMxRef)
	{
		KbtMixin._Kbt$mxEnable (this._Kbt$mxRoot,pMxRef);
	},
	/// pMxRef: Mixin
	/// overrideList: [{funcId: function}]
	/// Overrides given functions from mixin pMxRef in this object with given
	/// implementations. pMxRef must be guaranteed to be enabled in this object
	/// before you use this function.
	/// @return hash of implementations that were in effect before the override
	/// (name => un-overridden method). It's not a gateway, just a helper.
	mxOverride: function (pMxRef,overrideList)
	{
		// add entries from pOvList to our defines
		var overrides = {},prevImpl = {},
			mxRoot = this._Kbt$mxRoot,
			methods = mxRoot._Kbt$methods;
		kbtAssert (!mxRoot._Kbt$mxFrozen,"MxBase::mxOverride - can't override after MxBase::mxFreeze");
		for (let mtdName in overrideList)
		{
			let mtdId = pMxRef._Kbt$enablesMethod (mtdName);
			kbtAssert (mtdId!=undefined,
				"Kbt.Mixin.declare (enablesList,addsList,ctor): method '"+mtdName+"' is not defined in mixin #"+pMxRef._Kbt$mxId+" from enableList and can't be overridden");
			kbtAssert (KbtMixin._Kbt$regMethods[mtdId].isVirtual,
				"Kbt.Mixin.declare (enablesList,addsList,ctor): method '"+mtdName+"' is not virtual in mixin #"+pMxRef._Kbt$mxId+" from enableList and can't be overridden");
			overrides[mtdId] = overrideList[mtdName];
			prevImpl[mtdName] = methods[mtdId];
		}
		_kbt$applyDefinitions.call (mxRoot,methods,overrides);
		// override invalidates existing gateway
		mxRoot._Kbt$gateways[pMxRef._Kbt$mxId] = {};
		return prevImpl;
	},
	/// pMxRef: Mixin
	/// obj: any object
	/// Attempts to identify implementation of pMxRef in object obj (it has
	/// members with same names as all virtual functions in pMxRef and they
	/// are functions (assumed methods of obj)) and enable official
	/// implementation of pMxRef in this object that will route requests to obj.
	/// The call fails if pMxRef is already enabled in this object or if obj
	/// doesn't implement all required members as functions.
	/// @return gateway to identified implementation of pMxRef
	mxIdentify: function (pMxRef,obj)
	{
		var mxRoot = this._Kbt$mxRoot,
			methods = mxRoot._Kbt$methods;
		kbtAssert (pMxRef._Kbt$ctor==undefined,
			"MxBase::mxIdentify - mixin #"+pMxRef._Kbt$mxId+" has a constructor and can't be identified in arbitrary object");
		kbtAssert (!mxRoot._Kbt$gateways[pMxRef._Kbt$mxId],
			"MxBase::mxIdentify - mixin #"+pMxRef._Kbt$mxId+" is already enabled");
		var ovList = pMxRef._Kbt$identifyOverrideList (obj,true);
		kbtAssert (!ovList._Kbt$incomplImpl,
			"MxBase::mxIdentify - identifiable object doesn't implement all pure virtual functions");
		delete ovList._Kbt$incomplImpl;
		KbtMixin._Kbt$mxEnable (mxRoot,pMxRef);
		var overrides = {};
		for (let mtdName in ovList)
		{
			let mtdId = pMxRef._Kbt$enablesMethod (mtdName);
			kbtAssert (mtdId!=undefined,
				"MxBase::mxIdentify (pMxRef,obj): method '"+mtdName+"' is not defined in mixin #"+pMxRef._Kbt$mxId+" from enableList and can't be overridden");
			kbtAssert (KbtMixin._Kbt$regMethods[mtdId].isVirtual,
				"Kbt.Mixin.declare (enablesList,addsList,ctor): method '"+mtdName+"' is not virtual in mixin #"+pMxRef._Kbt$mxId+" from enableList and can't be overridden");
			overrides[mtdId] = ovList[mtdName];
		}
		_kbt$applyDefinitions.call (mxRoot,methods,overrides);
	},
	/// Returns original mixin implementator for a public mixin gateway. I. e.,
	/// if you obtained a gateway from an object via MxYourMixin.castFrom,
	/// you can obtain direct ref to that object back from the gateway with
	/// mxRoot.
	mxRoot: function ()
	{
		return this._Kbt$mxRoot;
	},
	/// Disables any further augmentation of the object with new mixins and
	/// overriding existing ones, also converts all methods to frozen versions
	/// that can be possibly more optimized for performance. Don't use inside
	/// mixins, use for final classes only.
	mxFreeze: function ()
	{
		var mxRoot = this._Kbt$mxRoot;
		if (!mxRoot._Kbt$mxFrozen)
		{
			mxRoot._Kbt$mxFrozen = true;
			// ...probably to do something else
		}
	}
});

/// ImxEnumerable
/// Should be implemented by classes/mixins that can serve as containers
/// providing sequential iteration through some their contents.
Kbt.ImxEnumerable = ImxEnumerable = Kbt.Mixin.declare ([],
// adds
{
	/// Return enumerator thru implementor's elements. Assumed use in form:
	/// for (var/let x in obj.elements ()) ...
	elements: Kbt.Func.virtual, // ()
	/// Return all elements currently present in implementor in an array. Uses
	/// elements to iterate thru elements.
	/// @treturn Array All elements currently present in implementor as an array
	elementsSnapshot: function ()
	{
		var result = [];
		for (var x in this.elements ()) result.push (x);
		return result;
	}
});

/// ImxEnumerableBiDi
/// Should be implemented by classes/mixins that can serve as containers
/// providing sequential iteration through some their contents and are able
/// to provide it in both directions.
Kbt.ImxEnumerableBiDi = ImxEnumerableBiDi = Kbt.Mixin.declare ([ImxEnumerable],
// adds
{
	/// Return enumerator thru implementor's elements in reverse order. Assumed
	/// use in form: for (var/let x in obj.elements ()) ...
	elementsReverse: Kbt.Func.virtual, // ()
	/// Return all elements currently present in implementor in an array, in
	/// reverse order. Uses elementsReverse to iterate thru elements.
	/// @treturn Array All elements currently present in implementor as a
	/// reversed array
	elementsReverseSnapshot: function ()
	{
		var result = [];
		for (var x in this.elementsReverse ()) result.push (x);
		return result;
	}
});

/// ImxCopyable
/// Should be implemented by classes/mixins that assume by-value use and should
/// be rather copied than passed by reference. Objects that implement
/// ImxCopyable are regarded by Kbt.copyObject as ones to be copied by value
/// (using their ImxCopyable::copy method), whenever they are copied themselves
/// or are immediate members of a copied object.
Kbt.ImxCopyable = ImxCopyable = Kbt.Mixin.declare ([],
// adds
{
	/// Return copy of the object. It's entirely up to implementation to
	/// construct what it deems to be its copy, it just has to be a new object.
	copy: Kbt.Func.virtual // ()
});

/// Class that wraps multiple exceptions stacked together. Can be thrown on
/// dispose or auto-dispose.
/// Implements: ImxEnumerable
Kbt.Exceptions = KbtExceptions = function ()
{
	KbtMixin.enable (this);
	
	var myElements = [];
	this.elements = function () // implementation of ImxEnumerable
	{
		for (var i in myElements) yield myElements[i];
	};

	/// Add an exception to the collection. If it's another Kbt.Exceptions,
	/// unroll it and append all its exceptions individually, so Kbt.Exceptions
	/// is never nested.
	this.push = function (exception)
	{
		if (exception instanceof KbtExceptions)
			// other Kbt.Exceptions - push unrolled
			for (var e in exception.elements ())
				myElements.push (e);
		else
			myElements.push (exception);
	};

	/// Remove first exception in the collection and shift it. Return undefined
	/// if the collection is empty
	this.shift = function ()
	{
		return myElements.length>0 ? myElements.shift () : undefined;
	};

	this.peek = function ()
	{
		return myElements.length>0 ? myElements[0] : undefined;
	};

	this.clear = function () { myElements.clear (); };
	this.count = function () { return myElements.length; };

	this.mxIdentify (ImxEnumerable,this);
}

/// Returns e if it's not a Kbt.Exceptions or returns first exception in the
/// collection (w/o removing it) if it is. When using Kbt extensions, always use
/// this function inside catch to get the actual current exception instead of
/// direct use of catch variable, because an exception may be (and may be not)
/// wrapped in Kbt.Exceptions.

/// A helper that you should always use in catch when using Kbt extensions. The
/// strategy is as following:
/// catch (rawEx) {
/// let e = Kbt.Exceptions.preCatch (rawEx,
///  function (e) e instanceof ExceptionClassYouWant);
/// ...e contains the exception you wanted, or the control doesn't get here...
/// }
/// @param e Exception you obtained from catch (e)
/// @tparam predicate Function One-argument function returning bool, should
/// return true if its parameter is an exception you are expecting, or false
/// otherwise
/// @return The actual exception conforming predicate if e included it wrapped
/// or unwrapped, or rethrows otherwise. If the exception was wrapped and
/// conformed predicate but there were other exceptions, drops it from the
/// collection and rethrows the rest (it conforms with multi-exception catching
/// convention assumed by Kbt).
KbtExceptions.preCatch = function (e,predicate)
{
	if (e instanceof KbtExceptions)
	{
		var ex = e.peek ();
		if (predicate (ex))
		{
			e.shift ();
			if (e.count ()>0) throw e;
			return ex;
		}
		else throw e;
	}
	else if (predicate (e)) return e;
	else
		throw e;
};

/// MxDisposable
/// Should be implemented by classes/mixins that intend to provide explicit,
/// deterministic deinitialization (destructors). Such objects can be switched
/// to disposed state by calling MxDisposable::dispose method. Also, supports
/// reference counting that allows you to share a MxDisposable implementing
/// object by several locations without fear to dispose it while it is still
/// needed.
/// You do not override dispose function directly. Instead, your class/mixin
/// constructor should set up a 'disposal agenda', a sequence of actions (which
/// can be either functions to call or objects implementing MxDisposable to
/// dispose) that will be called on dispose in LIFO order. Every action is
/// guaranteed to be called, even if any of them throw an exception. If any
/// action from disposal agenda throws an exception, these exceptions accumulate
/// and are propagated to caller of dispose, packed in Kbt.Exceptions.
/// Kbt framework provides handy syntax extensions to make use of MxDisposable
/// easier. #disposable ClassNameopt (...) { ... } is a shortcut for
/// function ClassNameopt (...) { ... } that implicitly augments the constructor
/// with enabling of MxDisposable, and also automatically triggers current
/// disposal agenda if the constructor throws an exception. Also, later in this
/// constructor, use this.#~ { code } to add an action to dispose agenda or
/// this.#~(expression_result_is_MxDisposable) to add a MxDisposable object.
/// this.#~ { ... } is a statement, while this.#~(...) is an expression that
/// returns the object added and allows to write like this:
/// var disposableMember = this.#~(new MyMxDisposable (...));.
Kbt.MxDisposable = MxDisposable = KbtMixin.declare ([],
// adds
{
	/// Perform disposal agenda. It is performed in LIFO order. Every item is
	/// guaranteed an invocation attempt, even if previous items throw an
	/// exception. If exceptions occured during dispose, they are wadded into
	/// KbtExceptions and thrown as it.
	/// Performing dispose clears the dispose agenda, so you are secured from
	/// disposing twice. Also, you are secured from recursive call (e. g., when
	/// dispose is invoked from a dispose agenda), there will be simply no
	/// effect in this case.
	/// If the object has been retained (retain), its retain counter will be
	/// decremented by 1, and dispose will only be performed when it reaches 0.
	dispose: Kbt.Func.placeholder, // ()
	/// Add a function to call or a MxDisposable implementor to dispose to
	/// dispose agenda
	/// @param funcOrDisposable, function or a disposable. Function is called
	/// in an indefinite context, so it's recommended to make it via
	/// Kbt.Func.bind (or #delegate).
	/// @return The same item you passed in funcOrDisposable. It allows syntax
	/// like var autoDisposeMember = this.markToDispose (new SomeMxDisposable ());
	/// @exceptions Error If the item is not function or a MxDisposable 
	markToDispose: Kbt.Func.placeholder, // (funcOrDisposable item) => item
	/// Increase reatin counter for the object. Object that enables MxDisposable
	/// starts with retain counter equal to 0. Calling retain will increase it
	/// by 1. If the retain counter is above 1, subsequent dispose will not
	/// do actual dispose but will just decrease counter by 1 instead.
	/// Use retain if you are going to share same MxDisposable object between
	/// several users with unpredictably varying lifetime.
	/// @return The object itself (mxRoot of the MxDisposable). It allows syntax
	/// like var sharedAutoDisposeMbr = someMxDisposable.retain ();
	retain: Kbt.Func.placeholder, // () => this
	/// Get current value of retain counter. Initial value for retain counter on
	/// creation of MxDisposable is 0.
	/// @treturn Number Current retain counter value
	retainCount: Kbt.Func.placeholder, // () => Number
	/// Returns true if the object is disposed
	/// @treturn Boolean True if the object is disposed, false otherwise
	isDisposed: Kbt.Func.placeholder // () => Boolean
},
// ctor
function ()
{
	var agenda = [],
		retainCount = 0,
		disposeInProgress = false;
	
	// dispose
	this.dispose.implement (function ()
	{
		if (disposeInProgress || --retainCount>0) return;
		disposeInProgress = true;
		var i = agenda.length,
			exs = null; // KbtExceptions
		while (--i>=0)
		try { agenda[i] (); }
		catch (e)
		{
			if (exs==null) exs = new KbtExceptions ();
			exs.push (e);
		}
		agenda.length = 0;
		if (exs!=null) throw exs;
	});

	// markToDispose implementation
	this.markToDispose.implement (function (funcOrDisposable)
	{
		var pAsDisposable;
		if (pAsDisposable = MxDisposable.dynamicCast (funcOrDisposable))
			agenda.push
				(kbtBind (pAsDisposable,function () { this.dispose (); }));
		else if (funcOrDisposable instanceof Function)
			agenda.push (funcOrDisposable);
		else throw new Error ("MxDisposable::markToDispose: item must be a parameterless function or implement MxDisposable");
		return funcOrDisposable;
	});

	// retain implementation
	this.retain.implement (function ()
	{
		++retainCount;
		return this.mxRoot ();
	});

	// retainCount implementation
	this.retainCount.implement (function ()
	{
		return retainCount;
	});

	// isDisposed implementation
	this.isDisposed.implement (function ()
	{
		return disposeInProgress && !agenda.length;
	});
});

// helper for #auto
_Kbt$AutoScope = function ()
{
	KbtMixin.enable (this);
	this.mxEnable (MxDisposable);
	this.exceptions = new KbtExceptions ();
}


/// Double-linked list that allows to iterate through its elements and manage
/// their membership explicitly by exposing access to list nodes.
/// Implements: ImxEnumerableBiDi
Kbt.List = KbtList = function ()
{
	KbtMixin.enable (this);

	var firstNode = null,lastNode = null,count = 0,revision = {};

	/// Return number of entries currently in list
	/// @treturn Number Elements count
	this.count = function () { return count; };

	/// Add an object or a node before a given node of this list
	/// @tparam nodeBefore Kbt.ListNode Node to add the entry before. Must be a
	/// node already belonging to this list or null if you want to add the entry
	/// to the end of the list
	/// @tparam nodeOrObj Kbt.ListNode|Object Object or node to add. If it's a
	/// node, it must not be currently in any list
	/// @treturn Kbt.ListNode Newly added node (nodeOrObj itself if it was a
	/// node)
	this.addBefore = function (nodeBefore,nodeOrObj)
	{
		kbtAssert (nodeBefore===null ||
			(nodeBefore instanceof KbtListNode && nodeBefore._Kbt$list===this),
			"Kbt.List.addBefore: nodeBefore must be null or a node from this list");
		if (nodeOrObj instanceof KbtListNode)
		{
			// a node
			kbtAssert (nodeOrObj._Kbt$list===null,"Kbt.ListNode already in list");
			nodeOrObj._Kbt$list = this;
			if (nodeBefore===null)
			{
				let prevLast = lastNode;
				nodeOrObj._Kbt$prev = prevLast;
				nodeOrObj._Kbt$next = null;
				if (prevLast!==null) prevLast._Kbt$next = nodeOrObj;
				else firstNode = nodeOrObj;
				lastNode = nodeOrObj;
			}
			else
			{
				let prevPrev = nodeBefore._Kbt$prev;
				nodeOrObj._Kbt$prev = prevPrev;
				nodeOrObj._Kbt$next = nodeBefore;
				nodeBefore._Kbt$prev = nodeOrObj;
				if (prevPrev!==null) prevPrev._Kbt$next = nodeOrObj;
				else firstNode = nodeOrObj;
			}
			count++;
			revision = {};
			return nodeOrObj;
		}
		else // other class of object
			this.addBefore (nodeBefore,new KbtListNode (nodeOrObj));
	};

	/// Add an object or a node after a given node of this list
	/// @tparam nodeBefore Kbt.ListNode Node to add the entry after. Must be a
	/// node already belonging to this list or null if you want to add the entry
	/// to the beginning of the list
	/// @tparam nodeOrObj Kbt.ListNode|Object Object or node to add. If it's a
	/// node, it must not be currently in any list
	/// @treturn Kbt.ListNode Newly added node (nodeOrObj itself if it was a
	/// node)
	this.addAfter = function (nodeAfter,nodeOrObj)
	{
		kbtAssert (nodeAfter===null ||
			(nodeAfter instanceof KbtListNode && nodeAfter._Kbt$list===this),
			"Kbt.List.addAfter: nodeAfter must be null or a node from this list");
		if (nodeOrObj instanceof KbtListNode)
		{
			// a node
			kbtAssert (nodeOrObj._Kbt$list===null,"Kbt.ListNode already in list");
			nodeOrObj._Kbt$list = this;
			if (nodeAfter===null)
			{
				let prevFirst = firstNode;
				nodeOrObj._Kbt$prev = null;
				nodeOrObj._Kbt$next = prevFirst;
				if (prevFirst!==null) prevFirst._Kbt$prev = nodeOrObj;
				else lastNode = nodeOrObj;
				firstNode = nodeOrObj;
			}
			else
			{
				let prevNext = nodeAfter._Kbt$next;
				nodeOrObj._Kbt$next = prevNext;
				nodeOrObj._Kbt$prev = nodeAfter;
				nodeAfter._Kbt$next = nodeOrObj;
				if (prevNext!==null) prevNext._Kbt$prev = nodeOrObj;
				else lastNode = nodeOrObj;
			}
			count++;
			revision = {};
			return nodeOrObj;
		}
		else // other class of object
			return this.addAfter (nodeAfter,new KbtListNode (nodeOrObj));
	};

	/// Add a node or an object to end of this list
	/// @tparam nodeOrObj Kbt.ListNode|Object Object or node to add. If it's a
	/// node, it must not be currently in any list
	/// @treturn Kbt.ListNode Newly added node (nodeOrObj itself if it was a
	/// node)
	this.addLast = function (nodeOrObj)
	{
		return this.addAfter (lastNode,nodeOrObj);
	};

	/// Add a node or an object to beginning of this list
	/// @tparam nodeOrObj Kbt.ListNode|Object Object or node to add. If it's a
	/// node, it must not be currently in any list
	/// @treturn Kbt.ListNode Newly added node (nodeOrObj itself if it was a
	/// node)
	this.addFirst = function (nodeOrObj)
	{
		return this.addBefore (firstNode,nodeOrObj);
	};

	/// Remove a node from the list
	/// @tparam nodeOrObj Kbt.ListNode|Object Node to remove. It must be a node
	/// currently in this list
	this.remove = function (node)
	{
		kbtAssert (node instanceof KbtListNode &&
			node._Kbt$list===this,
			"Kbt.List.remove: node must be a Kbt.ListNode currently in the list");
		var prevNext = node._Kbt$next,
			prevPrev = node._Kbt$prev;
		if (prevNext!==null) prevNext._Kbt$prev = prevPrev;
		else lastNode = prevPrev;
		node._Kbt$list = node._Kbt$next = node._Kbt$prev = null;

		if (prevPrev!==null) prevPrev._Kbt$next = prevNext;
		else firstNode = prevNext;

		count--;
		revision = {};
	};

	/// Removes first node in list (if any) and returns it (or null if list was
	/// empty)
	/// @treturn Kbt.ListNode Node previously first on list or null if the list
	/// was empty
	this.removeFirst = function ()
	{
		if (firstNode)
		{
			var result = firstNode;
			this.remove (firstNode);
			return result;
		}
		else return null;
	};

	/// Removes first node in list (if any) and returns it (or null if list was
	/// empty)
	/// @treturn Kbt.ListNode Node previously first on list or null if the list
	/// was empty
	this.removeLast = function ()
	{
		if (lastNode)
		{
			var result = lastNode;
			this.remove (lastNode);
			return result;
		}
		else return null;
	};

	/// Remove all current entries from the list
	this.clear = function ()
	{
		var next;
		for (var n=firstNode; n!==null; n=next)
		{
			n._Kbt$list = n._Kbt$prev = null;
			next = n._Kbt$next;
			n._Kbt$next = null;
		}
		firstNode = lastNode = null;
		count = 0;
		revision = {};
	};

	/// Return an iterator through the entries
	/// @return Iterator to use in for (var x in theIterator)
	this.nodes = function ()
	{
		var curRevision = revision;
		for (var n=firstNode; n!==null; n=n._Kbt$next)
		{
			kbtAssert (curRevision===revision,
				"Kbt.List.nodes: list has changed during iteration");
			yield n;
		}
	};

	/// Return list's first node, null if list is empty
	this.firstNode = function () { return firstNode; }

	/// Return list's last node, null if list is empty
	this.lastNode = function () { return lastNode; }

	this.elements = function ()
	{
		var curRevision = revision;
		for (var n=firstNode; n!==null; n=n._Kbt$next)
		{
			kbtAssert (curRevision===revision,
				"Kbt.List.nodes: list has changed during iteration");
			yield n.value;
		}
	}

	this.elementsReverse = function ()
	{
		var curRevision = revision;
		for (var n=lastNode; n!==null; n=n._Kbt$prev)
		{
			kbtAssert (curRevision===revision,
				"Kbt.List.nodes: list has changed during iteration");
			yield n.value;
		}
	}

	this.mxIdentify (ImxEnumerableBiDi,this);
	this.mxFreeze ();
}

/// Node in a Kbt.List. Does not implement any mixins.
Kbt.ListNode = KbtListNode = function (value)
{
	this._Kbt$list = null; // KbtList
	this._Kbt$next = this._Kbt$prev = null; // KbtListNode
	this.value = value;

	/// Get the list node is in or null if it's not in a list
	/// @treturn Kbt.List List this node is (null if none)
	this.list = function () { return this._Kbt$list; }

	/// Get prev node in the list
	/// @treturn Kbt.ListNode Prev node in this list (null if none)
	this.prev = function () { return this._Kbt$prev; }

	/// Get next node in the list
	/// @treturn Kbt.ListNode Next node in this list (null if none)
	this.next = function () { return this._Kbt$next; }
}

/// Returns a trigger - object that encapsulates call to multiple delegates
/// registered in a dynamically built invocation list (like C# delegates).
/// These functions are assumed to return nothing and to take same set of
/// parameters.
/// Create a trigger: var/let t = Kbt.createTrigger (); then invoke
/// it as a regular function: t (params);
/// Subscribe delegates (created with Kbt.bind or #delegate):
/// var subscription = t.add (#delegate { ... });
/// (note that subscribed item must be exactly the delegate, not a naked
/// function)
/// Subscription is a gateway to MxDisposable that you may use to unsubscribe:
/// subscription.dispose ();
Kbt.newTrigger = kbtNewTrigger = function ()
{
	kbtAssert (!(this instanceof kbtNewTrigger),
		"Kbt.Trigger - use Kbt.Trigger (), not new Kbt.Trigger () to create");

	var invList = new KbtList ();
	function doer ()
	{
		for (var f in invList.elements ())
			f.apply (this,arguments);
	}

	function Subscription (node)
	{
		Kbt.Mixin.enable (this);
		this.mxEnable (MxDisposable);
		this.markToDispose (function () { invList.remove (node); });
	}

	doer.add = function (f)
	{
		kbtAssert (f.boundTo!==undefined,
			"Kbt.newTrigger - can only subscribe a delegate (use Kbt.bind of #delegate)");

		var node = invList.addLast (f);
		return new Subscription (node);
	}

	return doer;
};

// Coroutine observer
Kbt.Observer = _Kbt$Observer =
	function (observersList,condition,isInterruptor,action)
{
	KbtMixin.enable (this);
	this.mxEnable (MxDisposable);

	this._Kbt$isInterruptor = isInterruptor;
	this._Kbt$condition = condition; // function () { let vname=msg; return expr; }
	this._Kbt$code = action;
	this._Kbt$suspendLevel = 0;
	this.suspend = function ()
	{
		this._Kbt$suspendLevel++;
		//return kbtBind (this,function () { this._Kbt$suspendLevel--; });
		return new function (me)
		{
			KbtMixin.enable (this);
			this.mxEnable (MxDisposable);
			this.markToDispose (function () { me._Kbt$suspendLevel--; });
		} (this);
	};
	this.resume = function ()
	{
		this._Kbt$suspendLevel--;
		//return kbtBind (this,function () { this._Kbt$suspendLevel++; });
		return new function (me)
		{
			KbtMixin.enable (this);
			this.mxEnable (MxDisposable);
			this.markToDispose (function () { me._Kbt$suspendLevel++; });
		} (this);
	}

	var node = observersList.addLast (this);
	this.markToDispose (function () { observersList.remove (node); });
};

_Kbt$crtMsgReenter = 'Kbt coroutine object re-entered';
_Kbt$crtMsgDisposeUnsafe = 'Kbt coroutine object disposed in unsafe state';

//
// event queue support
//

/// Implements event queue. The idea is as follows: you subscribe coroutines to
/// an event queue, then post event into the event queue, and the event is
/// offered as message to all subscribed coroutines. To optimize performance,
/// events are classified in types (therefore an object used as event must have
/// a string field named 'eventType'), and subscription of a coroutine requires
/// to specify set of event types that coroutine will be listening to. Events
/// of unlistened type are not offered to the coroutine.
/// An event may be consumed (if it triggers #expect or #[observe] of a matched
/// coroutine) or observed (if it triggers #observe). An event can be observed
/// by many observers but can only be consumed by one expector/interruptor. In
/// latter case, it's not offered to any observers/expectors remaining, but it
/// might have been handled by observers that happened to be earlier in
/// listeners queue. Since it's not possible to predict who exactly will observe
/// the event, it's recommended to separate event types by those that should
/// only be observed and those that should only be consumed, and identify these
/// classes by different eventType.
/// If the event is not consumed, it's dropped from the queue. However, you may
/// post it with postPersistent, then the event will not be dropped and will
/// keep being re-enqueued for next handling cycle until consumed. Use it in
/// case when your event must be consumed but you are not sure that a consumer
/// exists at time of you post it. (Use persistence with caution, as it may
/// litter the queue on misuse. Don't use it for events that are assumed to be
/// observed, for they will be observed on each attempt.)
/// Your event object may implement ImxDroppableEvent mixin if you want to
/// handle fact of dropping an event in some special way (to show an alert,
/// e. g.).
/// The event is 'static' if it's an object that enables MxStaticEvent mixin.
/// Static event must be posted with separate functions, postStatic and
/// postStaticPersistent, which are basically the same as post and
/// postPersistent. The difference is that same MxStaticEvent enabled object
/// can't be posted again until it's consumed or dropped. In exchange, you can
/// check its status (whether it is currently posted or not) and to withdraw it
/// from the queue manually. Event withdrawn will not be offered to any
/// listeners.
/// When the queue handling cycle is completed, the queue goes idle (expects for
/// new events). Before it does, an onIdle trigger is triggered. You can hook on
/// it to perform some actions on queue going idle.
/// That's all you have to know about event queues.
Kbt.MxEventQueue = MxEventQueue = KbtMixin.declare ([],
// adds
{
	/// Subscribe a coroutine into queue. Object to subscribe must be either one
	/// created via 'new CoroutineFunctionName ()' or one returned by #[self]
	/// from inside coroutine function. Return MxDisposable that can be disposed
	/// to cancel this subscription.
	/// Second parameter is array of strings each of which describes an event
	/// type this subscriber will listen. May be omitted (or contain of single
	/// item "*") which means that the subscriber will listen to any type of
	/// event. Using this is not recommended for common use however, for each of
	/// matching subscribers will have to be checked, and having too much of
	/// them will have negative impact on performance. To prevent it, try to
	/// specify only those event types in subscription that are really needed
	/// for its particular task, and to have as few observers subscribed for
	/// a particular event type as possible.
	subscribe: Kbt.Func.placeholder, // (Crt crt,[] evtTypes) => MxDisposable

	/// Posts an event into the queue. If the queue is idle, event is handled
	/// before post returns, otherwise (event processing is in progress) it is
	/// enqueued.
	/// Event can be any object that has "eventType" field of String type.
	/// This field is used for preliminary filtering. The event will not be
	/// offered to a subscribed coroutine unless its eventType matches one of
	/// event types specified by coroutine in subscribe.
	/// Will panic if event has MxStaticEvent mixin enabled. Events that enable
	/// MxStaticEvent must be posted using other function, postStatic.
	post: Kbt.Func.placeholder, // (event)

	/// Same as post, but an event will not be dropped from the queue until
	/// consumed.
	postPersistent: Kbt.Func.placeholder, // (event)

	/// Posts a static event into the queue. Panics if event has no
	/// MxStaticEvent mixin enabled, or if the event is already posted and not
	/// yet processed.
	postStatic: Kbt.Func.placeholder, // (MxStaticEvent event)

	/// Same as postStatic, but an event will not be dropped from the queue
	/// until consumed (or withdrawn).
	postStaticPersistent: Kbt.Func.placeholder, // (MxStaticEvent event)

	/// Returns a trigger that is triggered when the queue goes idle
	triggerOnIdle: Kbt.Func.placeholder, // () => Trigger

	/// Process accumulated events. Has no effect if re-entered (i. e., called
	/// from a coroutine). Use in main event loop to process events after you
	/// have posted them.
	processEvents: Kbt.Func.placeholder, // ()
},
function ()
{
	var queue = new KbtList (),
		handling = false,
		endMarker = new KbtListNode (),
		subsEndMarker = new KbtListNode (),
		newEvents = false,
		onIdleTrigger = kbtNewTrigger (),
		subscribersByType = // 'classId' => KbtList
		{
			// a 'check all events' list, all events will be offered to
			// listeners from it
			"*": new KbtList ()
		};

	queue._Kbt$eventQueue = this; // a secret internal var
	this.processEvents.implement (function ()
	{
		if (!handling)
		{
			handling = true;
			try
			{
				for (; newEvents;)
				{
					newEvents = false;
					queue.addLast (endMarker);
					var nowNode;
					for (nowNode=queue.firstNode (), queue.remove (nowNode);
						nowNode!=endMarker;
						nowNode=queue.firstNode (), queue.remove (nowNode))
					{
						var theEvent = nowNode.value,eventConsumed = false;
						kbtAssert (theEvent.eventType!==undefined,
							"An event must have eventType member defined");
						var ets = [theEvent.eventType, "*"];

						SUBS_ITERATE:
						for (var et in ets)
						{
							let typeToCheck = ets[et];
							// subscribersByType, pick one by one (using
							// subsEndMarker as iteration end marker), offer
							// theEvent to each one until consumed
							var subscribers = subscribersByType[typeToCheck]
								|| (subscribersByType[typeToCheck] = new KbtList ());
							subscribers.addLast (subsEndMarker);
							var nowSubsNode;
							// pick subscriber, moving him to end of queue
							// at the same time (somewhat verbose, isn't it)
							for (nowSubsNode=subscribers.firstNode (),
								subscribers.remove (nowSubsNode),
								subscribers.addLast (nowSubsNode);
								nowSubsNode!=subsEndMarker;
								nowSubsNode=subscribers.firstNode (),
								subscribers.remove (nowSubsNode),
								subscribers.addLast (nowSubsNode))
							{
								var theSubs = nowSubsNode.value;
								theSubs (theEvent); // offer event to subscriber
								if (theSubs.msgConsumed)
								{
									// event consumed, offer it to no one more.
									eventConsumed = true;
									break SUBS_ITERATE;
								}
							}
							subscribers.remove (subsEndMarker);
						}
						// remove subscriber queue end marker
						if (subsEndMarker.list ()!=null)
							subsEndMarker.list ().remove (subsEndMarker);
						if (!eventConsumed)
						{
							// event not consumed
							if (nowNode.persistent)
								// persistent event - re-enqueue
								queue.addLast (nowNode);
							else
							{
								// not persistent event - try ImxDroppableEvent
								var droppable =
									ImxDroppableEvent.dynamicCast (theEvent);
								if (droppable) droppable.runOnDrop ();
							}
						}
					}
	            }
	            // events are over, no new ones posted
	            onIdleTrigger ();
            }
            catch (e)
            {
            	// on exception, the queue is emergently cleared, and the
            	// exception is propagated to original poster
            	queue.clear ();
            	newEvents = false;
            	throw e;
            }
            finally
            {
            	handling = false;
            	if (endMarker.list ()!=null)
            		endMarker.list ().remove (endMarker);
				if (subsEndMarker.list ()!=null)
            		subsEndMarker.list ().remove (subsEndMarker);
            }
		}
	});

	this.post.implement (function (event)
	{
		kbtAssert (!MxStaticEvent.dynamicCast (event),
			"MxEventQueue: post can't be applied to MxStaticEvent, use postStatic");
		var node = queue.addLast (event);
		node.persistent = false;
		newEvents = true;
	});

	this.postPersistent.implement (function (event)
	{
		kbtAssert (!MxStaticEvent.dynamicCast (event),
			"MxEventQueue: postPersistent can't be applied to MxStaticEvent, use postStaticPersistent");
		var node = queue.addLast (event);
		node.persistent = true;
		newEvents = true;
	});

	this.postStatic.implement (function (event)
	{
		var staticEvent = MxStaticEvent.dynamicCast (event) ||
			kbtPanic ("MxEventQueue: postStatic can only be applied to MxStaticEvent");
		staticEventLink = staticEvent.mxRoot ()._Kbt$staticEventLink;
		!staticEventLink.list ()
			|| kbtPanic ("MxEventQueue: MxStaticEvent is already in a queue");
		queue.addLast (staticEventLink);
		staticEventLink.persistent = false;
		newEvents = true;
	});

	this.postStaticPersistent.implement (function (event)
	{
		var staticEvent = MxStaticEvent.dynamicCast (event) ||
			kbtPanic ("MxEventQueue: postStaticPersistent can only be applied to MxStaticEvent");
		staticEventLink = staticEvent.mxRoot ()._Kbt$staticEventLink;
		!staticEventLink.list ()
			|| kbtPanic ("MxEventQueue: MxStaticEvent is already in a queue");
		queue.addLast (staticEventLink);
		staticEventLink.persistent = true;
		newEvents = true;
	});

	this.triggerOnIdle.implement (function () { return onIdleTrigger; });

	this.subscribe.implement (function (crt,evtTypes)
	{
		var result = new function ()
		{
			KbtMixin.enable (this);
			this.mxEnable (MxDisposable);

			if (evtTypes===undefined) evtTypes = ["*"];
			var types = [];
			for (var type in evtTypes)
				types[evtTypes[type]] = new KbtListNode (crt);
			this.markToDispose (function ()
			{
				var node;
				for (var type in types)
				{
					node = types[type];
					if (node.list ()!=null) node.list ().remove (node);
				}
			});
			for (var type in types)
				(subscribersByType[type] ||
					(subscribersByType[type] = new KbtList ()))
				.addLast (types[type]);
		} ();
		newEvents = true;
		return result;
	});
});

Kbt.MxStaticEvent = MxStaticEvent = Kbt.Mixin.declare([MxDisposable],
// adds
{
	/// Returns event queue the event is posted in or null if it isn't posted
	/// (or is already processed).
	postedIn: Kbt.Func.placeholder, // () => MxEventQueue

	/// Withdraws the event from the queue immediately without processing. Event
	/// can be posted again. Ok to call if the event is not posted
	withdraw: Kbt.Func.placeholder, // ()
},
function ()
{
	// secret internal var
	var
		staticEventLink =
		this.mxRoot ()._Kbt$staticEventLink =
			new KbtListNode (this.mxRoot ());

	this.postedIn.implement (function ()
	{
		return staticEventLink.list () &&
			staticEventLink.list ()._Kbt$eventQueue; // use secret internal var
	});

	this.withdraw.implement (function ()
	{
		var list = staticEventLink.list ();
		if (list) list.remove (staticEventLink);
	});
	this.markToDispose (kbtBind (this,function () { this.withdraw (); }));
});

Kbt.ImxDroppableEvent = ImxDroppableEvent = Kbt.Mixin.declare([],
// adds
{
	/// Is invoked when event object implementing this mixin is dropped from
	/// the queue without consumption
	runOnDrop: Kbt.Func.virtual // ()
});

//
// preprocessor/compiler support part
//

/// Kbt preprocessor. Responsible for passes 1 and 2.
Kbt.Preprocessor = KbtPreprocessor = new function ()
{
	var scopes =
		[ {macros: {}, vars: {}} ], // 'global' scope ([0])
		curScope = -1; // -1 = 'beyond global', next pushScope sets it to global
	this._Kbt$pushScope = function ()
	{
		// reset new scope unless it's global
		if (++curScope>0) scopes[curScope] = { macros: {}, vars: {} };
	};
	this._Kbt$popScope = function ()
	{
		// clear closed scope unless it's global
		if (curScope-->0) scopes[curScope+1] = undefined;
	};
	/// Get variable in current or one of above PP scopes by name,
	/// returns undefined if no such var or its value if there is
	/// @tparam String name Name of the variable
	/// @return Variable value or undefined if no such variable in any of
	/// scopes on the stack
	this.getVar = function (name)
	{
		for (var i=curScope,scope=scopes[i]; i>=0; scope=scopes[--i])
		if (scope.vars[name]!==undefined) return scope.vars[name];
		return undefined;
	};
	/// Set/replace variable with given name in current PP scope
	/// @tparam String name Name of the variable
	/// @param value Value of the variable to set, undefined to remove
	/// the variable
	this.letVar = function (name,value)
	{
		scopes[curScope].vars[name] = value;
	};
	/// Set variable with given name in current PP scope or higher where it
	/// is defined, raise error if it's defined nowhere
	/// @tparam String name Name of the variable
	/// @param value Value of the variable to set, undefined to remove
	/// the variable
	this.setVar = function (name,value)
	{
		for (var i=curScope,scope=scopes[i]; i>=0; scope=scopes[--i])
		if (scope.vars[name]!==undefined)
		{
			scope.vars[name] = value;
			return;
		}
		throw new Error ("preprocessor variable '"+name+"' is undefined");
	};
	/// Returns true if name is a reserved name that can't be used as macro
	/// identifier (i. e., 'for', 'define', etc.)
	this.isReservedMacroName = function (name)
	{
		return name=="echo" ||
			name=="error" ||
			name=="define" ||
			name=="undef" ||
			name=="switch" ||
			name=="case" ||
			name=="default" ||
			name=="statement" ||
			name=="for" ||
			name=="let" ||
			name=="set";
	};
	/// Returns true if macro with a given name is defined in current or one
	/// of above scopes. Note that macro can exist in higher scope but be masked
	/// via explicit undef in one of intermediate scopes.
	this.isDefined = function (name)
	{
		return this._Kbt$findMacro (name)!==undefined;
	};
	/// Takes noise, if it is a single identifier, returns it as string,
	/// otherwise returns false
	/// @param noise Noise to check
	this.asIdentifier = function (noise)
	{
		if (noise[0]!==undefined
			&& noise.length==1
			&& noise[0].type=="id") return noise[0].text;
		else
			return false;
	};
	/// Takes string and returns noise that represents a single identifier
	/// with name equal to this string
	/// @tparam name String Identifier to make noise of
	/// @treturn Array Noise corresponding to single identifier with that name
	/// @exceptions Error If string provided is not a valid identifier
	this.makeIdentifier = function (name)
	{
		if (!name.match (/^[A-Za-z_$][A-Za-z0-9_$]*$/))
			throw Error ("(makeIdentifier) '"+name+"' is not a valid identifier");
		return [{ type: "id", text: name, space: " ", line: -1 }];
	};
	/// Takes string, number, boolean or unidentified and returns noise that
	/// represents literal of the corresponding value
	/// @tparam name String Identifier to make noise of
	/// @treturn Array Noise corresponding to single identifier with that name
	/// @exceptions Error If value provided is of invalid type
	this.makeLiteral = function (value)
	{
		if (!name.match (/^[A-Za-z_$][A-Za-z0-9_$]*$/))
			
		if (typeof value=="string")
			return [{
				type: "dqstring",
				line: -1,
				space: " ",
				text: '"'+value.replace(/\\/g,"\\\\")
					.replace(/"/g,"\\\"")
					.replace(/\r\n|\n\r|\r|\n/g,"\\$&")+'"'
				}];
		else if (typeof value=="number")
			return [{
				type: "dqstring",
				line: -1,
				space: " ",
				text: value.toString ()
				}];
		else if (typeof value=="boolean" || typeof value=="undefined")
			return [{
				type: "id",
				line: -1,
				space: " ",
				text: value.toString ()
				}];
		else	
			throw Error ("(makeLiteral) '"+typeof value+"' is not an allowed type");
	};
	/// Takes noise and converts it to its string representation
	/// @tparam noise String Noise to stringify
	/// @treturn String representation of the noise
	this.noiseToString = function (noise)
	{
		var result = "";
		for (var i in noise)
		{
			if (!noise[i].ppCmd)
				result += (noise[i].preSpace || "")+noise[i].text+noise[i].space;
		}
		return result;
	}

	this._Kbt$define = function (name,value)
	{
		scopes[curScope].macros[name] = value;
	};
	this._Kbt$undef = function (name)
	{
		scopes[curScope].macros[name] = false;
	};
	this._Kbt$findMacro = function (name)
	{
		for (var i=curScope,scope=scopes[i]; i>=0; scope=scopes[--i])
		if (scope.macros[name]!==undefined)
			return scope.macros[name] || undefined;
		return undefined;
	};
};

/// Maximum preprocessor scope (command) nesting threshold. Exceeding it aborts
/// preprocessor with an error. Used as protection against running into infinite
/// recursion. Default value (128) is generally reasonable. Change as you need.
/// @type Number
KbtPreprocessor.maxRecursionLevelAllowed = 128;

/// Subiteration is entering a preprocessor command at level above 1 (topmost).
/// Example:
/// not_subiteration <# not_subiteration <# subiteration #> #>
/// Maximum number of consecutive subiterations without returning to level 1 is
/// limited to this number, exceeding it aborts preprocessor with an error. Used
/// as protection against running into combinatorial explosion on expansion of
/// nested loops (namely, #@for and tree-like recursions).
/// One of implications is that maximum number of iterations in a #@for is
/// limited to this number (or even less if the #@for is a subiteration). So, if
/// it doesn't fit you, increase this value.
/// @type Number
KbtPreprocessor.maxSubiterationsAllowed = 65536;

/// Function to be performed on #@echo () command. You can replace it with what
/// you need.
/// @tparam str String Message to print
KbtPreprocessor.echo = function (str)
{
	document.write ("<b>echo:</b> "+Kbt.htmlSpecialChars (str)+"<br>\n");
}

/*
 * parseScripts
 * (c) James Padolsey
 * http://james.padolsey.com
 * Adapted for Kbt framework needs by Mikle[Subterranean Devil]
 */

/// Compile Kbt extended JS <script>-s and append them to the document so they
/// are executed
Kbt.parseScripts = function () {
    var
    	scriptType = Kbt.Compiler.scriptType,
    	parseFn = Kbt.Compiler.compile,
    	scripts = document.getElementsByTagName('script'),
        sLength = scripts.length,
        execute = function(parsed) {
            // Execute parsed script in global context.
            var dScript = document.createElement('script');
            dScript.type = Kbt.Compiler.targetType;
            try {
                dScript.appendChild( document.createTextNode(parsed) );
                document.body.appendChild(dScript);
            } catch(e) {
                dScript.text = parsed;
                document.getElementsByTagName('head')[0].appendChild(dScript);
            }
            dScript.parentNode.removeChild(dScript);
        };
    
    for (var i=0; i<sLength; i++) {
        // All script elements matching scriptType are passed to parseFn.
        var script = scripts[i],
            type = script.type,
            code = script.innerHTML;
        if (scriptType.test ? scriptType.test(type) : type === scriptType) {
            let srcName = "<document>";
            if (script.src) {
            	srcName = script.src;
                var xhr = window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
                xhr.open('GET', script.src, false);
                xhr.send(null);
                code = xhr.responseText;
                xhr = null;
            }
            // TODO (?): calculate valid line number
            execute(parseFn ? parseFn(code,srcName,1) : code);
        }
    }
    
}

Kbt.tokenEqual = function (lhsToken,rhsToken)
{
	// both args are null - equal
	if (!lhsToken && !rhsToken) return true;
	// one of args is null, other is not - unequal
	if ((!lhsToken)!=(!rhsToken)) return false;
	// one of args is not a token, other one is - compare text only
	if ((typeof lhsToken=="string" || typeof lhsToken=="number") &&
		rhsToken.text!==undefined)
		return lhsToken==rhsToken.text;
	if ((typeof rhsToken=="string" || typeof rhsToken=="number") &&
		lhsToken.text!==undefined)
		return lhsToken.text==rhsToken;
	// both args are tokens - compare both type and text
	if (lhsToken.type!==undefined && rhsToken.type!==undefined &&
		lhsToken.text!==undefined && rhsToken.text!==undefined)
		return lhsToken.type==rhsToken.type && lhsToken.text==rhsToken.text;
	// if neither of above, unequal
	return false;
};

Kbt.Compiler = {};

/// Type (as per <script type="...">) that are recognized as Kbt extended JS
/// when Kbt.parseScripts is invoked. Default is "KbtJSX".
/// @type String
Kbt.Compiler.scriptType = "KbtJSX";
/// Type to be assigned to compiled script. Default is
/// "application/javascript;version=1.7" (note: version of target JS must >=1.7)
Kbt.Compiler.targetType = "application/javascript;version=1.7";

/// Compiles JS with Kbt extensions from srcJS source text
/// @tparam String srcJS Source text to compile
/// @tparam Number lineOffset Line number that the source text is assumed to
/// start at (default is 1)
/// @treturn String JS 1.7+ source that can be used in eval or otherwise
/// @exception SyntaxError On Kbt extension syntax errors, note the text for
/// line info and the message
Kbt.Compiler.compile = function (srcJS,fileName,lineOffset)
{
	// split a text of JS (with Kbt extensions) to array of tokens:
	// { type: token-type, text: token-text(verbatim),
	// line: token-line, space: skippable-spaces-and-comments-after-the-token,
	// preSpace: skippable-spaces-and-comments-before-the-token(may be not
	// set) }
	// token types are: "begin" (temp token starting the sequence, stripped),
	// "number", "id" (obvious), "sqstring", "dqstring" (single and double
	// quoted string literals), "regex" (slash quoted regex literal), "punct"
	// (all other characters (treated as punctuators), one by one),
	// "noise" (sequence of tokens limited by properly balanced parenthesis,
	// brackets or braces, not created at tokenize stage; has extra properties:
	// noise: array-of-tokens, type: type-of-outmost-brackets ("()", "[]" or
	// "{}"), "KbtStat" (marker of a Kbt extension statement, not created
	// at tokenize stage)
	function tokenize (src,startLine)
	{
		var
		regexSkippable = // whitespaces or comments
			/^(\s+|(\/\/[^\r\n]*(\r\n|\n\r|\r|\n|$))|\/\*[\s\S]*?(\*\/|$))*/,
		regexNumber = // any sort of number
			/^((0[xX][0-9a-fA-F]+)|(([0-9]+|(?=\.[0-9]))((\.[0-9]+|\.?(?=[eE]))([eE][+-]?[0-9]+)?)?))/,
		regexIdentifier = // an identifier
			/^[a-zA-Z_$][a-zA-Z_$0-9]*/,
		regexSQString = // single quoted string
			/^'([^'\\]+|\\(\r\n|\n\r|\r|\n|[^\r\n]|$))*('|$)/,
		regexDQString = // double quoted string
			/^"([^"\\]+|\\(\r\n|\n\r|\r|\n|[^\r\n]|$))*("|$)/,
		regexVString = // verbatim string (Kbt extension)
			/^#"([^\s(]*)\(([\s\S]*?)(\)\1"|$)/,
		regexRegex = // a '/'-delimited regexp + flags
			/^\/([^\/\\]+|\\(\r\n|\n\r|\r|\n|[^\r\n]|$))*(\/[A-Za-z]*|$)/,
		regexKbtPunct = // Kbt extension specific punctuators
			/^(#@|@#|<#|#>|#:|\.\.\.)/,
		regexPunct = // any other punctuator (apply after all previous items)
			/^\S/,
		regexNewline = // for lines count
			/\r\n|\n\r|\r|\n/g,
		regexToEndOfLine = // for lexic error report
			/[\s\S].*?(\r\n|\n\r|\r|\n|$)/;

		// init tokens array an parser
		var tokens = Array ();
		var cursor = 0;
		var lineNumber = startLine,
			lastCountedLineOffset = 0;
		function setCursor ()
		{
			var toSkip = srcJS.substr (0,cursor);
			// strip the fragment to skip from the script
			srcJS = srcJS.substr (cursor);
			cursor = 0;
			// find all newlines in skipped fragment
			regexNewline.lastIndex = 0;
			for (var match=regexNewline.exec (toSkip); match!=null; lineNumber++)
				match=regexNewline.exec (toSkip);
		}
		setCursor (cursor);
		// init line counter
		regexNewline.lastIndex = 0;
	
		function updateLineNumber ()
		{
			for (; lastCountedLineOffset<cursor; lineNumber++)
			{
				let match = regexNewline.exec (srcJS);
				lastCountedLineOffset = match.index;
			}
		}
		// first token (start)
		var whitespace = regexSkippable.exec (srcJS);
		tokens.push ({ type: "begin",
			text: "",
			space: whitespace[0],
			line: lineOffset });
		cursor = whitespace[0].length;

		var prevTokenType = "begin",prevTokenText = "";
		for (setCursor (); srcJS.length>0; setCursor ())
		{
			let nextToken = null,match;
			// check for string
			if (match = regexSQString.exec (srcJS))
				nextToken =	{ type: "sqstring",
					text: match[0] };
			else if (match = regexDQString.exec (srcJS))
				nextToken =	{ type: "dqstring",
					text: match[0] };
			else if (match = regexVString.exec (srcJS))
				// convert to double-quoted string
				nextToken =	{ type: "dqstring",
					text: '"'+
						match[2].replace(/\\/g,"\\\\")
						.replace(/"/g,"\\\"")
						.replace(/\r\n|\n\r|\r|\n/g,
							function (pattern)
							{
								switch (pattern)
								{
								case "\r\n": return "\\r\\n\\\r\n";
								case "\n\r": return "\\n\\r\\\n\r";
								case "\r": return "\\r\\\r";
								case "\n": return "\\n\\\n";
								}
							})
						+'"' };
			else if (match = regexNumber.exec (srcJS))
				nextToken = { type: "number",
					text: match[0] };
			else if (match = regexIdentifier.exec (srcJS))
				nextToken = { type: "id",
					text: match[0] };
			else // check for regex, some context dependency here
			if (!(prevTokenType=="id" ||
				prevTokenType=="number" ||
				prevTokenType=="sqstring" ||
				prevTokenType=="dqstring" ||
				prevTokenType=="regex" ||
				(prevTokenType=="punct" &&
					(prevTokenText=="]" || prevTokenText==")")
				))
				&& (match=regexRegex.exec (srcJS)))
				nextToken = { type: "regex",
					text: match[0] };
			else if (match = regexKbtPunct.exec (srcJS))
			{
				nextToken = { type: "punct",
					text: match[0] };
				if (nextToken.text=="@#")
					nextToken.text = "#@"; // @# and #@ are equivalent
			}
			else if (match = regexPunct.exec (srcJS))
				nextToken = { type: "punct",
					text: match[0] };

			// check if a token is extracted ok
			if (nextToken)
			{
				prevTokenType = nextToken.type;
				prevTokenText = nextToken.text;
				nextToken.line = lineNumber;
				cursor = match[0].length;
				setCursor ();
				let match2 = regexSkippable.exec (srcJS);
				nextToken.space = match2[0];
				cursor = match2[0].length;
				tokens.push (nextToken);
			}
			else
			{
				throw new SyntaxError ("File "+fileName+", line +"+lineNumber+", lexer error at: "+
					regexToEndOfLine.exec (srcJS)[0]+"...");
			}
		}
		// strip "begin" token
		if (tokens.length>1)
		{
			tokens[1].preSpace = tokens[0].space;
			tokens.shift ();
		}
		return tokens;
	}

	// an iterator through tokens array obtanied in tokenize
	function TokensIterator (tokensArray)
	{
		var
			tokens = tokensArray,
			cursor = 0;
		this.length = tokens.length;
		// true if iteration is completed
		this.eof = function () { return cursor>=this.length; };
		// skip given number of tokens
		this.goNext = function (howmany)
		{ if ((cursor += howmany)>this.length) cursor = this.length; };
		// return current token at cursor (undefined if at eof)
		this.current = function (offset) { return tokens[cursor+offset]; };
		// compare if tokens subsequence starting at current cursor
		// (plus/minus offset) matches a given tokens array.
		// If token entry in array to match is specified as "ID", any
		// "id" type is expected
		this.match = function (tokensArrayToMatch,offset)
		{
			if (offset===undefined) offset = 0;
			var i = cursor+offset;
			for (var j in tokensArrayToMatch)
			{
				let toMatch = tokensArrayToMatch[j];
				if (!(toMatch=="ID" && !this.eof () && tokens[i].type=="id"))
				if (!Kbt.tokenEqual (tokens[i],toMatch))
					return false; // found an unmatch
				i++;
			}
			// found no unmatches
			return true;
		}
		// generate syntax error starting at current token
		this.genSyntaxError = function (msg)
		{
			var line = this.current (this.eof ()? -1 : 0).line;
			var clip = "",i,cur;
			for (i=0, cur=this.current (i);
				cur!==undefined && cur.line==line;
				i++, cur=this.current (i))
				clip += cur.text+cur.space;
			throw new SyntaxError ("File "+fileName+", line +"+line+", "+msg+" at: "+clip);
		}
		// expect a specific token or a token sequence, if it's at cursor, skip
		// cursor to next token, otherwise raise syntax error
		this.expect = function (tokens)
		{
			for (var i in tokens)
			if (
				(tokens[i]=="ID" && this.current (0).type=="id") ||
				Kbt.tokenEqual (this.current (0),tokens[i]))
				this.goNext (1);
			else this.genSyntaxError ("'"+tokens[i]+"' expected");
		}
		// same as expect but don't advance cursor
		this.assert = function (tokens)
		{
			this.expect (tokens);
			this.goNext (-tokens.length);
		}
		// make a full-fledged "punct" type token of its text given as a String.
		// Token is assumed to be at current line and file (provided it's not
		// a eof)
		this.punctToken = function (tokenText)
		{
			return { type: "punct",
				text: tokenText,
				line: tokens[cursor].line,
				space: "" };
		}
		// append n next tokens to array arr and advance the cursor
		this.read = function (arr,n)
		{
			for (var i=0; i<n; i++, this.goNext (1))
				arr.push (this.current (0));
		}
	}

	// preprocesses noise from the stream for parenthesis concatenation
	// operators (#:), 1st preprocessor pass. Insert result into dstNoise
	function preparsePass1 (it,dstNoise,delimiter)
	{
		function extractSegment () // #: [name:] { ... }/( ... )/[ ... ]
		{
			it.expect (["#:"]);
			var next = it.current (0),funcLen;
			if (it.match (["ID", ":"]))
			{
				// a "name:" follows - append and skip it
				dstNoise.push (next);
				dstNoise.push (it.current (1));
				it.goNext (2);
				next = it.current (0);
			}
			if ((it.match (["function","("]) && (funcLen=2)) ||
				(it.match (["#", "disposable","("]) && (funcLen=3)) ||
				(it.match (["#", "[", "function", "]", "("]) && (funcLen=5)))
			{
				//dstNoise.push (next);
				//dstNoise.push (it.current (1));
				//it.goNext (2);
				it.read (dstNoise,funcLen);
				// parse params noise
				preparsePass1 (it,dstNoise,")");
				it.expect ([ ")" ]);
				// append closing bracket
				dstNoise.push (it.current (-1));
				it.expect ([ "{" ]);
				// append opening brace
				dstNoise.push (it.current (-1));
				// parse func body
				preparsePass1 (it,dstNoise,"}");
				it.expect ([ "}" ]);
				// append closing brace
				dstNoise.push (it.current (-1));
			}
			else if (next.text=="{" || next.text=="[" || next.text=="(")
			{
				let delim;
				switch (next.text)
				{
				case "{": delim = "}"; break;
				case "[": delim = "]"; break;
				case "(": delim = ")"; break;
				}
				// append opening bracket
				dstNoise.push (next);
				it.goNext (1);
				// parse bracketed noise recursively
				preparsePass1 (it,dstNoise,delim);
				it.expect ([ delim ]); // ensure valid bracket follows
				// append closing bracket
				dstNoise.push (it.current (-1));
				// adjust trailing space for seamlessness
				dstNoise[dstNoise.length-1].space = " ";
			}
			else
				it.genSyntaxError ("'{', '[', '(' of 'function (...) {...}', possibly preceded by 'name:', expected");
		}
		for (; !it.eof () &&
			(delimiter===undefined || !it.match (delimiter));)
		{
			if (it.match (["(", ")", "#:"]))
			{
				// longest case - ()#:
				// append opening '('
				let openPar = it.current (0),
					closePar = it.current (1),
					first = true;
				dstNoise.push (openPar);
				// skip over 2 first tokens
				it.goNext (2);
				// extract all #: components
				for (;;)
				{
					if (!it.match (["#:"])) break;
					// comma delimiter for non-first entry
					if (!first)
						dstNoise.push (it.punctToken (","));
					first = false;
					extractSegment ();
				}
				// append closing parenthesis
				dstNoise.push (closePar);
			}
			else if (it.match ([")", "#:"]))
			{
				// medium case - )#:
				let closePar = it.current (0);
				// skip over closing parenthesis for now
				it.goNext (1);
				// extract all #: components
				for (;;)
				{
					if (!it.match (["#:"])) break;
					dstNoise.push (it.punctToken (","));
					extractSegment ();
				}
				// append closing parenthesis
				dstNoise.push (closePar);
			}
			else if (it.match (["#:"]))
			{
				// shortest case - #:
				// synthesize and append opening parenthesis
				dstNoise.push (it.punctToken ("("));
				let first = true;
				// extract all #: components
				for (;;)
				{
					if (!it.match (["#:"])) break;
					if (!first)
						dstNoise.push (it.punctToken (","));
					first = false;
					extractSegment ();
				}
				// synthesize and append closing parenthesis
				dstNoise.push (it.punctToken (")"));
			}
			else // TODO: )#: and #:
			{
				// not a pattern from #: group - append and skip a token
				dstNoise.push (it.current (0));
				it.goNext (1);
			}
		}
	}

	function noiseToString (noise)
	{
		var result = "";
		// TODO: control against preprocessor commands
		function dumpObject(obj,indentLvl)
		{
			var result = "<## ",indent = "";
			indentLvl = indentLvl || 0;
			for (var i=0; i<=indentLvl; i++) indent += " ";
			for (var j in obj)
				if (typeof obj[j]!="object" || obj[j]===null)
					result += indent+j+": '"+obj[j]+"'\n";
				else if (obj[j][0]!==undefined &&
					(obj[j][0].text!==undefined || obj[j][0].ppCmd!==undefined))
					// a subnoise
					result += indent+j+": <## "+noiseToString (obj[j])+" ##>\n";
				else
					// just an object
					result += indent+j+": "+dumpObject (obj[j],indentLvl+1)+"\n";
			result += "##>";
			return result;
		}
		for (var i in noise)
		{
			if (noise[i].ppCmd)
				result += dumpObject (noise[i]);
			else
				result += (noise[i].preSpace||"")+noise[i].text+noise[i].space;
		}
		return result;
	}

	// try parsing a noise enclosed in a given type of bracket, with option
	// whether to include bounding brackets or not in returned result
	// ("[", "(", "{", "<#")
	function parseBracketedNoise (it,bracketType,includeBracket)
	{
		var result = [];
		if (it.match ([bracketType]))
		{
			if (includeBracket) result.push (it.current (0));
			it.goNext (1);
			preparsePass2 (it,result,true);
			switch (bracketType)
			{
			case "[":
				it.expect (["]"]);
				if (includeBracket) result.push (it.current (-1));
				break;
			case "(":
				it.expect ([")"]);
				if (includeBracket) result.push (it.current (-1));
				break;
			case "{":
				it.expect (["}"]);
				if (includeBracket) result.push (it.current (-1));
				break;
			case "<#":
				it.expect (["#>"]);
				if (includeBracket) result.push (it.current (-1));
				break;
			}
			return result;
		}
		else
			return false;
	}

	// try parsing a preprocessor command from an iterator, return pseudo token
	// with it on success, or just a next token if no command recognized (or
	// throw an error if one encountered)
	function parsePreprocessorCommand (it,commandsOnly)
	{
		// body is a {}, [], (), #@statement {} or <##> block
		function parseBody ()
		{
			// try to parse a body
			// return { ppCmd: "body", type: "{"|"["|"("|"<#"|"statement",
			// body: noise as is (for <#, with <# #> excluded) }
			// or false if failed to detect any of these blocks at it.current(0)
			var result = false;
			if (it.match (["{"]))
				result = { ppCmd: "body",
					line: it.current (0).line,
					type: "{",
					body: parseBracketedNoise (it,"{",true) };
			else if (it.match (["["]))
				result = { ppCmd: "body",
					line: it.current (0).line,
					type: "[",
					body: parseBracketedNoise (it,"[",true) };
			else if (it.match (["("]))
				result = { ppCmd: "body",
					line: it.current (0).line,
					type: "(",
					body: parseBracketedNoise (it,"(",true) };
			else if (it.match (["function", "("]))
			{
				var body = [],par,fBody;
				body.push (it.current (0));
				par = parseBracketedNoise (it,"(",true);
				it.expect (["{"]);
				it.goNext (-1);
				fBody = parseBracketedNoise (it,"{",true);
				for (var i in par) body.push (par[i]);
				for (var i in fBody) body.push (fBody[i]);
				result = { ppCmd: "body",
					line: body[0].line,
					type: "function",
					body: body };
			}
			else if (it.match (["<#"]))
			{
				let space = it.current (0).space;
				result = { ppCmd: "body",
					type: "<#",
					line: it.current (0).line,
					body: parseBracketedNoise (it,"<#",false) };
				if (result.body.length>0)
				{
					// append proper spaces for seamless exclusion of <# #>
					result.body[0].preSpace = space;
					result.body[result.body.length-1].space +=
						it.current (-1).space;
				}
			}
			else if (it.match (["#@", "statement", "{"]))
			{
				it.goNext (2);
				result = { ppCmd: "body",
					line: it.current (0).line,
					type: "statement",
					body: parseBracketedNoise (it,"{",true) };
			}
			return result;
		}

		function parseBodyOrCommand ()
		{
			// parse {}/[]/()/#@statement {}/<##> or a preprocessor command
			var result = parseBody ();
			if (!result) result = parsePreprocessorCommand (it,true);
			return result;
		}

		var result;
		// detect commands
		if (it.match (["#@", ":", "ID"]))
		{
			// it's a preprocessor var reference
			// #@:var or #@:var[pptimeExpr]
			result = { ppCmd: "varref",
				line: it.current (0).line,
				varId: it.current (2).text };
			it.goNext (3);
			let index = parseBracketedNoise (it,"[",false);
			if (index)
			{
				// it's an indexed pp var ref
				result.varIndex = index;
			}
		}
		else if (it.match (["#@", "("]))
		{
			// it's a preprocessor time expr reference
			// #@(pptimeExpr)
			result = { ppCmd: "expr",
				line: it.current (0).line };
			it.goNext (1);
			let val = parseBracketedNoise (it,"(",false);
			if (!val)
				it.genSyntaxError ("'( js-expression )' expected");
			result.expr = val;
		}
		else if (it.match (["#@", "echo"]) || it.match (["#@", "error"]))
		{
			// it's a preprocessor time message output or error
			// #@echo (pptimeExpr) or @#error (pptimeExpr)
			result = { ppCmd: it.current (1).text,
				text: "#@error",
				space: "",
				line: it.current (0).line };
			it.goNext (2);
			let val = parseBracketedNoise (it,"(",false);
			if (!val)
				it.genSyntaxError ("'( js-expression )' expected");
			result.expr = val;
		}
		else if (it.match (["#@", "set"]) || it.match (["#@", "let"]))
		{
			// it's a #@set/#@let #@:var = (pptimeExpr)/[pptimeExpr]/{pptimeExpr}
			it.goNext (2);
			it.expect (["#@", ":"]);
			if (it.current (0).type!="id")
				it.genSyntaxError ("(set) variable identifier expected");
			result = { ppCmd: it.current (-3).text=="set"? "setvar" : "letvar",
				line: it.current (0).line,
				varId: it.current (0).text };
			it.goNext (1);
			it.expect (["="]);
			let val;
			if (it.match (["("]))
				val = parseBracketedNoise (it,"(",false);
			else if (it.match (["["]))
				val = parseBracketedNoise (it,"[",true);
			else if (it.match (["{"]))
				val = parseBracketedNoise (it,"{",true);
			if (!val)
				it.genSyntaxError ("(set) '(/[/{ js-expression )/]/}' expected");
			result.expr = val;
		}
		else if (it.match (["#@", "for"]))
		{
			// it's a #@for #@:varname in #@:varargname: body_or_op
			it.goNext (2);
			it.expect (["#@", ":"]);
			if (it.current (0).type!="id")
				it.genSyntaxError
					("(for) iterator variable identifier expected");
			result = { ppCmd: "for",
				line: it.current (0).line,
				iteratorId: it.current (0).text };
			it.goNext (1);
			it.expect (["in", "#@", ":"]);
			if (it.current (0).type!="id")
				it.genSyntaxError
					("(for) iterable vararg identifier expected");
			result.varId = it.current (0).text;
			it.goNext (1);
			it.expect ([":"]);
			result.space = it.current (-1).space;
			result.text = "#@for #@:"+result.iteratorId+" in #@:"+result.varId;
			result.cmd = parseBodyOrCommand ();
			if (!result.cmd)
				it.genSyntaxError
					("(for) '(', '[', '{', '<#', 'function (', '#@statement {' or preprocessor command expected");
		}
		else if (it.match (["#@", "switch"]))
		{
			// it's a #@switch #@case (pptimeExpr): body ...
			// #@case (pptimeExpr): body #@default: body
			it.goNext (2);
			result = { ppCmd: "switch",
				line: it.current (0).line,
				cases: [] };
			for (;;)
			{
				let nextCase = {};
				if (!it.match (["#@"])) break;
				// check against improper case/default
				if ((it.match (["#@", "case"]) ||
					it.match (["#@", "default"])) &&
					result.cases.length>0 &&
					result.cases[result.cases.length-1].condition==null)
					it.genSyntaxError ("(switch) only one '#@default' per switch is allowed and it must be the last case in list");
				// parse case
				if (it.match (["#@", "case"]))
				{
					it.goNext (2);
					if (it.match (["("]))
						nextCase.condition = parseBody ().body;
					else
						it.genSyntaxError ("(case) '( js-expression )' expected");
				}
				else if (it.match (["#@", "default"]))
				{
					it.expect (["#@", "default"]);
					nextCase.condition = null;
				}
				else
					break;
				it.expect ([":"]);
				nextCase.cmd = parseBodyOrCommand ();
				if (!nextCase.cmd)
					it.genSyntaxError
						("(case/default) '(', '[', '{', '<#', '#@statement {' or preprocessor command expected");
				result.cases.push (nextCase);
			}
		}
		else if (it.match (["#@", "define"]))
		{
			// it's #@define #@MACRO_ID (args) = body
			result = { ppCmd: "define",
				line: it.current (0).line };
			it.goNext (2);
			it.expect (["#@"]);
			if (it.current (0).type!="id")
				it.genSyntaxError
					("(define) identifier of the new macro expected");
			result.macroId = it.current (0).text;
			if (KbtPreprocessor.isReservedMacroName (result.macroId))
				it.genSyntaxError
					("(define) '"+result.macroId+"' is PP keyword and can't be used in this context");
			it.goNext (1);

			// parse args to array of { name: string, vararg: true/false,
			// defaultValue: null/noise }
			let args = [],namesUsed = {};
			if (it.match (["("]))
			{
				// parameters specification is present
				it.expect (["("]);
				for (;;)
				{
					if (it.eof ())
						it.genSyntaxError
							("(define) unterminated macro args list");
					if (it.match ([")"]))
						break; // reached end of params description
					// get next param identifier
					let paramId = it.current (0);
					if (paramId.type!="id")
						it.genSyntaxError
							("(define) identifier (parameter name) expected");
					if (namesUsed[paramId.text]!==undefined)
						it.genSyntaxError
							("(define) parameter name '"+paramId.text+"' is already used");
					else namesUsed[paramId.text] = true;
					let arg = { paramId: paramId.text };
					// get default
					it.goNext (1);
					if (it.match (["..."]))
					{
						// it's a vararg
						arg.vararg = true;
						it.goNext (1);
						// default value is not applicable to vararg
					}
					else if (it.match (["="]))
					{
						// it's an arg with default value spec
						arg.vararg = false;
						it.goNext (1);
						// extract the default value
						if (it.match (["{"]) ||
							it.match (["["]) ||
							it.match (["("]) ||
							it.match (["function", "("]) ||
							it.match (["<#", "#>"]))
							arg.defaultValue = parseBody ().body;
						else
							it.genSyntaxError
								("(define) parameter default value must start with '(', '[' or '{', or be empty ('<# #>')");
					}
					else if (it.match ([","]) || it.match ([")"]))
					{
						// it's an arg w/no default value spec
						arg.vararg = false;
						arg.defaultValue = null;
					}
					else
						it.genSyntaxError ("'...', '=', ',' or ')' expected");
					// parameter spec is parsed
					args.push (arg);
					// check that a valid delimiter or closing bracket follows
					if (it.match ([","]))
						it.goNext (1);
					else if (it.match ([")"]))
						break;
					else
						it.genSyntaxError ("',' or ')' expected");
				}
				it.expect ([")"]);
			}
			result.args = args;
			it.expect (["="]);
			// parse macro body
			if (!(result.body = parseBody ()))
				it.genSyntaxError ("macro body must start from '(', '[', '{', '#@statement {', 'function (' or '<#'");
			if (result.body.body.length>0)
			{
				// trim leading and trailing space to single one, in order to
				// provide most seamless expansion possible (i. e., one where
				// expanded fragment takes as same place in destination text as
				// macro invocation command)
				result.body.body[0].preSpace = " ";
				result.body.body[result.body.body.length-1].space = " ";
			}
		}
		else if (it.match (["#@", "undef"]))
		{
			// it's #@undef #@MACRO_ID
			result = { ppCmd: "undef",
				line: it.current (0).line };
			it.goNext (2);
			it.expect (["#@"]);
			if (it.current (0).type!="id")
				it.genSyntaxError
					("(undef) identifier of the macro to undef expected");
			result.macroId = it.current (0).text;
			if (KbtPreprocessor.isReservedMacroName (result.macroId))
				it.genSyntaxError
					("(undef) '"+result.macroId+"' is PP keyword and can't be used in this context");
			it.goNext (1);
		}
		else if (it.match (["#@", "ID"]))
		{
			// it's a macro invocation
			result = { ppCmd: "macro",
				line: it.current (0).line,
				macroId: it.current (1).text };
			if (KbtPreprocessor.isReservedMacroName (result.macroId))
				it.genSyntaxError
					("(macro invocation) '"+result.macroId+"' is PP keyword and can't be used in this context");
			result.text = "#@"+result.macroId; // for possible error reporting
			result.space = "";
			it.goNext (2);
			if (it.match (["("]))
			{
				// macro with args
				it.expect (["("]);
				let args = []; // => { name: null, arg: [] }
				for (;;)
				{
					if (it.match ([")"]) || it.eof ()) break; // end of args
					let arg = {};
					if (it.match (["ID", ":"]))
					{
						// named arg - fix and skip name
						arg.name = it.current (0).text;
						it.goNext (2);
					}
					else
						arg.name = null; // unnamed arg
					arg.arg = [];
					preparsePass2 (it,arg.arg,",");
					args.push (arg);
				}
				it.expect ([")"]);
				result.args = args;
			}
			else
				result.args = [];
		}
		else if (it.match (["<#"]))
		{
			// a raw preprocessor scope
			result = parseBody ();
		}
		else
		{
			// it's just a token
			if (commandsOnly) return false; // do not return single tokens
			// or do
			result = it.current (0);
			it.goNext (1);
		}
		return result;
	}

	// parse noise by given iterator with preprocessor commands recognized and
	// inserted into the stream
	function preparsePass2 (it,dstNoise,delimiter)
	{
		var bracketStack = [],bracketLineStack = [],bsc = -1;
		for (;;)
		{
			let cmd = parsePreprocessorCommand (it);
			if (cmd===undefined) break; // eof
			if (cmd.text!==undefined)
			{
				// it's a single token:
				// check delimiter in delimiter mode
				if (bsc<=-1 && cmd.text===delimiter) return;
				// bracket control
				switch (cmd.text)
				{
				case "{": bracketStack[++bsc] = "}";
					bracketLineStack[bsc] = it.current (-1).line; break;
				case "[": bracketStack[++bsc] = "]";
					bracketLineStack[bsc] = it.current (-1).line; break;
				case "(": bracketStack[++bsc] = ")";
					bracketLineStack[bsc] = it.current (-1).line; break;
				case "<#": bracketStack[++bsc] = "#>";
					// this will unlikely trigger, as <# #> by itself is
					// a naked noise command
					bracketLineStack[bsc] = it.current (-1).line; break;
				case "}":
				case "]":
				case ")":
				case "#>":
					if (bsc<=-1)
					{
						it.goNext (-1); // roll back 1 token to blocker bracket
						if (delimiter) return; // in delim mode, parse to bkt
						else
							it.genSyntaxError ("unexpected bracket '"+cmd.text+"'");
					}
					else if (bracketStack[bsc]!=cmd.text)
					{
						// bracket nesting mismatch
						it.goNext (-1); // roll back 1 token to locate error properly
						it.genSyntaxError ("bracket nesting mismatch, '"
							+bracketStack[bsc]+
							"' expected (opened at line "+bracketLineStack[bsc]+")");
					}
					else
						bsc--; // bracket closed
				}
			}
			dstNoise.push (cmd);
		}
		if (bsc>-1)
			// don't roll back this time, eof is valid position for the error
			it.genSyntaxError ("missing bracket '"+bracketStack[bsc]+
				"' (opened at line "+bracketLineStack[bsc]+")");
	}

	var
		ppRecLevel = 0, // recursion level
		ppIterations = 0; // number of iterations at level >1
	// create and run preprocessor scope on preprocessor-preparsed noise
	// srcNoise, return resulting noise
	// check for nesting level and count descents within a level 1, error
	// if they exceed threshold
	function preprocess (srcNoise)
	{
		// preprocess
		var result = [],cursor = 0;

		// raise a preprocessor error
		function preprocessorError (msg)
		{
			var line = srcNoise[cursor].line || srcNoise[cursor-1].line;
			var clip = "",i;
			for (i=cursor, cur=srcNoise[i];
				cur!==undefined && cur.line==line;
				i++, cur=srcNoise[i])
				clip += cur.text+cur.space;
			throw new Error ("File "+fileName+", line +"+line+", "+msg+" at: "+clip);
		}
		KbtPreprocessor._Kbt$pushScope ();
		// recursion limiter
		if (++ppRecLevel>KbtPreprocessor.maxRecursionLevelAllowed)
			preprocessorError ("KbtPreprocessor.maxRecursionLevelAllowed exceeded");

		// perform preprocessor command and return expanded result
		function doCommand (cmd)
		{
			// subiterations limiter
			if (++ppIterations>KbtPreprocessor.maxSubiterationsAllowed)
				preprocessorError ("KbtPreprocessor.maxSubiterationsAllowed exceeded");
			var prevSrcNoise = srcNoise,prevCursor = cursor;
			srcNoise = [cmd]; cursor = 0; // for error report
			function appendToResult (noise)
			{
				for (var i in noise)
				if (isNaN (+i))
					preprocessorError ("not a valid noise to emit");
				else
					result.push (noise[i]);
			}
			switch (cmd.ppCmd)
			{
			case "body":
				// cmd.type: "{"|"["|"("|"<#"|"statement",
				// cmd.body: noise,
				if (cmd.type=="statement")
					appendToResult ([
					{ type: "id", line: cmd.line, space: " ", text: "do" }
					]);
				var preprocessed = preprocess (cmd.body),afterspace = "";
				if (cmd.type=="statement" && preprocessed.length>0)
				{
					// correct spacing for "statement" scenario so that inlining
					// was prettier
					afterspace = preprocessed[preprocessed.length-1].space;
					preprocessed[preprocessed.length-1].space = " ";
				}
				appendToResult (preprocessed);
				if (cmd.type=="statement")
					appendToResult ([
					{ type: "id", line: cmd.line, space: " ", text: "while" },
					{ type: "punct", line: cmd.line, space: "", text: "(" },
					{ type: "number", line: cmd.line, space: "", text: "0" },
					{ type: "punct", line: cmd.line, space: afterspace, text: ")" }
					]);
				break;
			case "define":
				// cmd.macroId: string,
				// cmd.args: [] { paramId: string, vararg: bool, defaultValue: noise/null }
				// cmd.body: { type, body (as per "body") }
				KbtPreprocessor._Kbt$define (cmd.macroId,
					{ args: cmd.args, body: cmd.body });
				break;
			case "undef":
				// cmd.macroId: string
				KbtPreprocessor._Kbt$undef (cmd.macroId);
				break;
			case "for":
				// cmd.iteratorId: string
				// cmd.varId: string
				// cmd.cmd: nested command ({ ppCmd: ..., ... })
				var iteratedVar = KbtPreprocessor.getVar (cmd.varId);
				if (typeof iteratedVar!="object")
					preprocessorError ("'"+cmd.varId+"' is not a noise array var or a current macro vararg parameter");
				KbtPreprocessor._Kbt$pushScope ();
				try
				{
					for (var i in iteratedVar)
					{
						if (+i==NaN)
							preprocessorError ("'"+cmd.varId+"' is not a noise array var or a current macro vararg parameter");
						KbtPreprocessor.letVar (cmd.iteratorId,preprocess (iteratedVar[i]));
						doCommand (cmd.cmd);
					}
				}
				finally
				{
					KbtPreprocessor._Kbt$popScope ();
				}
				break;
			case "switch":
				// cmd.cases: [] { condition: noise/null, cmd: nested cmd }
				for (var i in cmd.cases)
				{
					let theCase = cmd.cases[i];
					if (theCase.condition==null ||
						eval (noiseToString (preprocess (theCase.condition))))
					{
						doCommand (theCase.cmd);
						break;
					}
				}
				break;
			case "macro":
				// cmd.macroId: string
				// cmd.args: [] { name: string/null, arg: noise }
				var macro = KbtPreprocessor._Kbt$findMacro (cmd.macroId);
				if (macro===undefined)
					preprocessorError ("'"+cmd.macroId+"' is undefined in this PP scope");
				// construct parameters array

				// consequently apply following rules:
				// 1) named parameters are explicitly mapped to named parameters, if one is
				// vararg, it's mapped to 1st vararg parameter,
				// 2) first leading unnamed parameter is mapped to 1st parameter,
				// 3) runs of unnamed parameters after each named one (and after first leading
				// unnamed one) are mapped to sequentially following parameters, if a parameter
				// is vararg, it is filled with all remaining of the run,
				// 4) parameters remaining unfilled after 1)-3) are assigned with defaults (if
				// they have one) or stay empty (if they are varargs),
				// 5) after 1)-4), if there still are unfilled vars or vars filled twice, it's
				// an error.

				// examples:
				// (x1,x2,x3), (a,b,c) => (x1: a, x2: b, x3: c)
				// (x1,x2,x3=d), (a,b,c) => (x1: a, x2: b, x3: c)
				// (x1,x2,x3=d), (a,b) => (x1: a, x2: b, x3: d)
				// (x1,x2=d,x3), (x1: a,b, x3: c) => (x1: a, x2: b, x3: c)
				// (x1,x2=d,x3), (x1: a, x3: c) => (x1: a, x2: d, x3: c)
				// (x1,x2=d,x3), (a,b,x3: c) => (x1: a, x2: b, x3: c)
				// (x1,x2=d,x3), (a,x3: c) => (x1: a, x2: d, x3: c)
				// (x1...,x2=d1,x3=d2), (a,b,c) => (x1: [a,b,c], x2: d1, x3: d2)
				// (x1...,x2=d1,x3=d2), (a,b,x2: d,e) => (x1: [a,b], x2: d, x3: e)
				// (x1,x2...,x3=d), (a,b,c) => (x1: a, x2: [b,c], x3: d)
				// (x1,x2...,x3), (x3: d, x1: a,b,c) => (x1: a, x2: [b,c], x3: d)
				// (x1,x2...,x3), (x1: a,b,c, x3: d) => (x1: a, x2: [b,c], x3: d)
				// (x1,x2...,x3), (x1: a,b,c,d) => error - (x1: a, x2: [b,c,d], x3: UNDEFINED)
				// (x1,x2,x3), (x1: a,b, x2: c,d) => error - (x1: a, x2: CONFLICT(b/c), x3: d)

				// prepare array of parameters to fill
				var i,params = [], // { vararg: bool, args: [] noise, defaultValue: null/noise }
					param1stIndex = {}; // index in params by name
				// macro { args:
				// { name: string, vararg: true/false, defaultValue: null/noise }
				// body: pass to doCommand
				// }
				var macroArgs = macro.args;
				for (i in macroArgs)
				{
					params.push ({
						name: macroArgs[i].paramId,
						vararg: macroArgs[i].vararg,
						defaultValue: macroArgs[i].defaultValue,
						args: [] });
					param1stIndex[macroArgs[i].paramId] = params.length-1;
				}

				// split params provided into runs
				var runs = [], // { name1st: string, params: [] noise }
					currentRun = { name1st: null, params: [] };
				for (i in cmd.args)
				{
					let nextArg = cmd.args[i];
					if (nextArg.name!==null)
					{
						// named arg:
						// save current run (if there was any)
						if (currentRun.params.length>0)
							runs.push (currentRun);
						// and start next one with this arg
						currentRun =
							{ name1st: nextArg.name, params: [nextArg.arg] };
					}
					else
						// unnamed arg:
						// append to previous run
						currentRun.params.push (nextArg.arg);
				}
				// save last run (if there was any)
				if (currentRun.params.length>0)
					runs.push (currentRun);
				// if first run is unnamed, assign it name of 1st expected param
				if (runs.length>0 && runs[0].name1st===null)
					if (macroArgs.length>0)
						runs[0].name1st = macroArgs[0].paramId;
					else
						runs[0].name1st = undefined;
				// distribute parameters provided to parameters expected
				var nMaxParam = params.length;
				for (i in runs)
				{
					let run = runs[i];
					// process each run
					if (run.name1st===undefined)
					{
						// it can only happen if macro has no parameters and
						// user provided some
						srcNoise = run.params[0];
						cursor = 0;
						preprocessorError ("stray macro parameter");
					}
					let j = param1stIndex[run.name1st],fillVararg = false;
					if (j===undefined) // wrong parameter name
					{
						srcNoise = run.params[0];
						cursor = 0;
						preprocessorError
							("macro '"+cmd.macroId+"' has no parameter '"+run.name1st+"'");
					}
					for (let i1 in run.params)
					{
						if (!fillVararg)
						{
							// check if param is not stray
							if (j>=nMaxParam)
							{
								srcNoise = run.params[i1];
								cursor = 0;
								preprocessorError ("stray macro parameter");
							}
							// check if param is not specified twice
							if (params[j].args.length>0)
							{
								srcNoise = run.params[i1];
								cursor = 0;
								preprocessorError ("macro parameter '"+params[j].name+"' is specified more than once");
							}
							// add param
							params[j].args.push (run.params[i1]);
							fillVararg = params[j].vararg;
							if (!fillVararg) j++;
						}
						else
						{
							// params[j] is pointing at vararg - add it there
							params[j].args.push (run.params[i1]);
						}
					}
				}
				// check that all required parameters are specified, fill defaults
				for (i in params)
				{
					let param = params[i];
					if (!param.vararg && param.args.length<=0)
					if (param.defaultValue!=null)
						// param unspecified, but there is default provided
						param.args.push (param.defaultValue);
					else
						preprocessorError ("macro parameter '"+param.name+"' is not specified and has no default");
				}
				// create scope and set variables
				KbtPreprocessor._Kbt$pushScope ();
				try {
					// fill variables
					for (i in params)
					{
						let param = params[i];
						if (param.vararg)
						{
							let noises = [];
							for (let j in param.args)
								noises.push (preprocess (param.args[j]));
							KbtPreprocessor.letVar (param.name,noises);
						}
						else
							KbtPreprocessor.letVar
								(param.name,preprocess (param.args[0]));
					}
					doCommand (macro.body);
				}
				finally	{ KbtPreprocessor._Kbt$popScope (); }
				break;
			case "echo":
			case "error":
				// cmd.expr: noise
				var value = eval (noiseToString (preprocess (cmd.expr)));
				if (cmd.ppCmd=="echo") KbtPreprocessor.echo (value.toString ());
				else preprocessorError (value.toString ());
				break;
			case "expr":
				// cmd.expr: noise
			case "varref":
				// cmd.varId: string
				// cmd.index: noise/undefined
				// these commands are very alike and can be handled similarly
				var value,exprNoise,indexNoise;
				if (cmd.ppCmd=="expr")
				{
					value = eval (noiseToString (preprocess (cmd.expr)));
					exprNoise = cmd.expr;
				}
				else // cmd.ppCmd=="varref"
				{
					value = cmd.index!==undefined?
						KbtPreprocessor.getVar (cmd.varId)
						[eval (noiseToString (preprocess (cmd.index)))]
						: KbtPreprocessor.getVar (cmd.varId);
					exprNoise = [{
						type: "id", // fake type, token is needed just to show in error
						text: "#@:"+cmd.varId,
						space: "",
						line: cmd.line
						}];
				}
				if (typeof value=="string")
					// result is string
					appendToResult ([
					{
						type: "dqstring",
						line: cmd.line,
						space: " ",
						text: '"'+value.replace(/\\/g,"\\\\")
							.replace(/"/g,"\\\"")
							.replace(/\r\n|\n\r|\r|\n/g,"\\$&")+'"'
					}]);
				else if (typeof value=="number")
					// result is number
					appendToResult ([
					{
						type: "dqstring",
						line: cmd.line,
						space: " ",
						text: value.toString ()
					}]);
				else if (typeof value=="boolean")
					// result is boolean
					appendToResult ([
					{
						type: "id",
						line: cmd.line,
						space: " ",
						text: value.toString ()
					}]);
				else if (typeof value=="undefined") {}
				else if (typeof value=="object")
				{
					if (value[0]!==undefined &&
						value[0][0]!==undefined)
					{
						// looks like array of noises
						let first = true;
						for (let i in value)
						{
							if (+i==NaN)
								preprocessorError ("not a valid array of noises to emit");
							let tokens = preprocess (value[i]);
							if (!first) result.push ({
								type: "punct",
								space: "",
								text: ",",
								line: value[i].line
								});
							first = false;
							appendToResult (tokens);
						}
					}
					else
					{
						// noise
						appendToResult (value);
					}
				}
				else
				{
					// some corrections to display valid piece of code
					srcNoise = exprNoise;
					cursor = 0;
					preprocessorError ("bad pp-expr/var result to emit (is "+typeof value+"; must be noise, array of noises, string, bool or number)");
				}
				break;
			case "letvar":
				// cmd.varId: string
				// cmd.expr: noise
				KbtPreprocessor.letVar (cmd.varId,
					eval (noiseToString (preprocess (cmd.expr))));
				break;
			case "setvar":
				// cmd.varId: string
				// cmd.expr: noise
				KbtPreprocessor.setVar (cmd.varId,
					eval (noiseToString (preprocess (cmd.expr))));
				break;
			}

			srcNoise = prevSrcNoise; cursor = prevCursor; // restore
		}

		try
		{
			for (cursor in srcNoise)
			{
				let cmd = srcNoise[cursor];
				switch (cmd.ppCmd)
				{
				case "body":
				case "define":
				case "undef":
				case "for":
				case "switch":
				case "macro":
				case "expr":
				case "echo":
				case "error":
				case "varref":
				case "letvar":
				case "setvar":
					doCommand (cmd);
					break;
				default:
					// a plain token or an unimplemented command (should not
					// get here actually)
					result.push (cmd);
				}
			}
		}
		finally
		{
			KbtPreprocessor._Kbt$popScope ();
			ppRecLevel--;
			if (ppRecLevel<=1) ppIterations = 0;
		}
		return result;
	}

	// expand a token or a syntax extension statement object to line
	// of source code (for error reporting)
	function expandClip (stat)
	{
		var clip = "";
		if (stat instanceof Array)
			for (var i in stat) clip += expandClip (stat[i]);
		else if (stat.tokens)
		{
			var tokens = stat.tokens;
			for (var i in tokens)
				clip += expandClip (tokens[i]);
		}
		else if (stat.text || stat.space)
			clip = stat.text+stat.space;
		return clip;
	}
	// raise a compiler error
	function compilerError (it,msg)
	{
		var line = it.current (0).line || it.current (-1).line;
		var clip = "";
		for (cur=it.current (0);
			cur!==undefined && cur.line==line;
			it.goNext (1), cur=it.current (0))
			clip += expandClip (cur);
		throw new SyntaxError ("File "+fileName+", line +"+line+", "+msg+" at: "+clip);
	}

	var
	implDelBody = "kbtBind(this,function%PARAM%%BODY%)",
	implAutoPrologue =
		"{let _Kbt$as=new _Kbt$AutoScope();try",
	implAutoEpilogue =
		"catch(e){_Kbt$as.exceptions.push(e);}"+
		"finally{try{_Kbt$as.dispose();}catch(e){_Kbt$as.exceptions.push(e);}"+
		"finally{if(_Kbt$as.exceptions.count()>0)throw _Kbt$as.exceptions;}}}";
	implStatBody =
		"%OBJECT%markToDispose(kbtBind(this,function()%BODY%));",
	implExprBody =
		"%OBJECT%markToDispose(%BODY%)",
	implDisposablePrologue =
		"{KbtMixin.enable(this);this.mxEnable(MxDisposable);try",
	implDisposableEpilogue =
		"catch(e){this.markToDispose(function(){throw e;});this.dispose();}}",
		
	// crtfunction specific things
	implStatPrologue = "do {",
	implStatEpilogue = "} while (false)",

	implCrtFuncPrologue =
	"{"+
		"var "+
			"core,"+
			"_Kbt$msg,"+
			"_Kbt$reply,"+
			"_Kbt$consumed=false,"+
			"_Kbt$done=false,"+
			"_Kbt$result,"+
			"_Kbt$isRunning=false,"+
			"_Kbt$unhandledExc=false,"+
			"_Kbt$func=function(msg)"+
		"{"+
			"try"+
			"{"+
				"kbtAssert(!_Kbt$isRunning,_Kbt$crtMsgReenter);"+
				"_Kbt$isRunning=true;"+
				"_Kbt$msg=msg;"+
				"core.next();"+
				"return copyResult();"+
			"}"+
			"catch(e)"+
			"{"+
				"if(!_Kbt$unhandledExc)"+ // likely a StopIteration
					"return copyResult();"+
				"throw e;"+
			"}"+
			"finally"+
			"{_Kbt$isRunning=false;}"+
		"};"+
		"function Core ()"+
		"{"+
			"if(false) yield undefined;"+ // to make obvious it's an iterator
			"try"+
			"{"+
				"var _Kbt$observers=new KbtList();"+
				"function _Kbt$doObservers()"+
				"{"+
					"for(let observer in _Kbt$observers.elements())"+
					"{"+
						"if (observer._Kbt$suspendLevel<=0&&"+
							"observer._Kbt$condition(_Kbt$msg))"+
						"{"+
							"if(observer._Kbt$isInterruptor)"+
							"{"+
								"intrTriggered=true;"+
								"let func=new observer._Kbt$code();"+
								"_Kbt$consumed=true;"+
								"try{"+
									"observer._Kbt$suspendLevel++;"+
									"for(let x in func)"+
									"{"+
										"yield true;"+
										"_Kbt$reply=undefined;"+
									"}"+
								"}"+
								"finally"+
								"{observer._Kbt$suspendLevel--;}"+
								"break;"+
							"}"+
							"else "+
								"observer._Kbt$code (_Kbt$msg);"+
						"}"+
					"}"+
				"}"+
				implAutoPrologue,

	implObserveElement = // #observe ... id: (cond) { action } && ...
		"%ID% = new _Kbt$Observer (_Kbt$observers,"+
			"kbtBind(this,function(%ID%)%COND%),"+
			"false,"+
			"kbtBind (this,function(%ID%)%ACTION%));"+
		"_Kbt$as.markToDispose(%ID%);",

	implInterruptorElement = // #[observe] ... id: (cond) { action } && ...
		"{"+
			"let _Kbt$innerIt=function(%ID%)"+
			"{"+
				"if(false)yield undefined;"+
				implAutoPrologue+
				"%ACTION%"+
				implAutoEpilogue+
			"};"+
			"%ID%=new _Kbt$Observer(_Kbt$observers,"+
				"kbtBind(this,function(%ID%)%COND%),"+
				"true,"+
				"function()"+
				"{return new _Kbt$innerIt(_Kbt$msg);});"+
			"_Kbt$as.markToDispose (%ID%);"+
		"}",

	implExpectPrologue = // #expect 
		"for(;;)"+
		"{"+
			"yield true;"+
			// ignore undefined msgs (we can't compare them adequately)
			"if(_Kbt$msg===undefined){_Kbt$consumed=false;continue;}"+
			"_Kbt$reply=undefined;"+
			"let intrTriggered=false,observerChecker=_Kbt$doObservers();"+
			// check observers
			"try{"+
				"for(let observer in observerChecker)"+
				"{"+
					"intrTriggered=true;"+
					"yield true;"+
					"_Kbt$reply=undefined;"+
				"}"+
			"}finally{observerChecker.close();}"+
			"if(intrTriggered)continue;"+ // interruptor hit - msg consumed
			"%INITCHK%", // example: [e1,e2,e3] = [_Kbt$msg,_Kbt$msg,_Kbt$msg];
			// _Kbt$consumed = true
	implExpectElement =
			"if%COND%"+ // (expr(e1))
			"{"+
				"%MASK%;"+ // example: [e2,e3] = [undefined,undefined];
				"break;"+
			"}"+
			"else ",
	implExpectEpilogue =
				"_Kbt$consumed = false;"+
		"}",

	// #checkpoint
	implCheckpoint = // uses same prologue as expect and a simplified ending
		kbtFormat (implExpectPrologue, { "INITCHK": "", })
		+"_Kbt$consumed=false;break;}";

	// #reply
	implReply =
		" _Kbt$reply ", // has semantic of an expression

	// #[return]
	implReturn =
		"_Kbt$result = %VAL%;"+
		"return;",

	// #[expr] (arg)
	implCall =
		"let func=new(%EXPR%)%ARG%,"+
			"prevRepl=_Kbt$reply;"+
		"try{"+
			"while(!func.done)"+
			"{"+
				//"_Kbt$reply=func.reply;"+
				"yield true;"+
				"for(;;)"+
				"{"+
					"let intrTriggered=false,observerChecker=_Kbt$doObservers();"+
					// check observers
					"try{"+
						"for(let observer in observerChecker)"+
						"{"+
							"intrTriggered=true;"+
							"yield true;"+
							"_Kbt$reply=undefined;"+
						"}"+
					"}finally{observerChecker.close();}"+
					"if(!intrTriggered)break;"+
					// interruptor hit - msg consumed, try next
					"yield true;"+
				"}"+
				"func(_Kbt$msg);"+
				"_Kbt$reply=func.reply;"+
				"_Kbt$consumed=func.msgConsumed;"+
			"}"+
		"}"+
		"finally {func.dispose()};"+
		"_Kbt$reply = prevRepl;",

	implCallAssignEpilogue = // => (expr) part of call
		"%EXPR% =func.result;",
				
	implCrtFuncEpilogue =
				implAutoEpilogue+
			"}"+
			"catch(e){_Kbt$unhandledExc=true;throw e;}"+
			"finally{_Kbt$done=true;}"+
		"}"+
		"var core=new Core(),result;"+
		"function copyResult()"+
		"{"+
			"_Kbt$func.done=_Kbt$done;"+
			"_Kbt$func.result=_Kbt$result;"+
			"_Kbt$func.reply=_Kbt$reply;"+
			"_Kbt$func.msgConsumed=_Kbt$consumed;"+
			"return !_Kbt$done;"+
		"}"+
		"KbtMixin.enable(_Kbt$func);"+
		"_Kbt$func.mxEnable(MxDisposable);"+
		"_Kbt$func.markToDispose(function()"+
		"{"+
			"kbtAssert (!_Kbt$isRunning,"+
				"_Kbt$crtMsgDisposeUnsafe);"+
			"core.close();"+
		"});"+
		"_Kbt$func();"+ // first advance (no message)
		"return _Kbt$func;"+
	"}";

	// recognizes Kbt extension stat in tokens array and returns it marked up
	// with "KbtStat" spec tokens and with tokens grouped into appropriate
	// noises. Type of statement is specified with "text" property of "KbtStat"
	// token.
	// Statements (statType key and tokens array if set) are:
	// "delegate": # delegate [(...)] { noise }
	// => [ '#', 'delegate', noise[(...)], noise[{ noise }] ]
	// "auto": # auto { noise }
	// => [ '#', 'auto', noise [{ noise }] ]
	// "autoexpr": [not .] # ~ ( noise )
	// => [ '#', '~', noise[( noise )] ]
	// "autostat": [not .] # ~ { noise }
	// => [ '#', '~', noise[{ noise }] ]
	// "objexpr": [after .] # ~ ( noise )
	// => [ '#', '~', noise[( noise )] ]
	// "objstat": [after .] # ~ { noise }
	// => [ '#', '~', noise[{ noise }] ]
	// "disposable": # disposable [id] ( noise ) { noise }
	// => [ '#' 'disposable', id/'', noise [( noise )], noise [{ noise }] ]
	// "crtfunction": # [ function ] [id] ( noise ) { noise }
	// => [ '#', '[', 'function', ']', id/'', noise [( noise )], noise [{ noise }] ]
	// "jsfunction": function [id] ( noise ) { noise }
	// => [ 'function', id/'', noise [( noise )], noise [{ noise }] ]
	// "expect": # expect id : ( noise ) || id : ( noise ) ... [not id]
	// => [ '#', 'expect', id, ':', noise [( noise )], '|', '|', id, ':' ... ]
	// "observe": # observe id ( noise ) { noise } || id ( noise ) { noise } ... [not id]
	// => [ '#', 'observe', id, ':', noise [( noise )], noise [{ noise }], '&', '&', id, ':' ... ]
	// "crtobserve": # [ observe ] id ( noise ) { noise } || id ( noise ) { noise } ... [not id]
	// => [ '#', '[', 'observe', ']', id, ':', noise [( noise )], noise [{ noise }], '&', '&', id, ':' ... ]
	// "reply": # reply [=/+=/..., or as expr]
	// => [ '#', 'reply' ]
	// "checkpoint": # checkpoint
	// => [ '#', 'checkpoint' ]
	// "crtthis": # [ this ]
	// => [ '#', '[', 'this', ']' ]
	// "crtreturn": # [ return ] ( noise )
	// => [ '#', '[', 'return', ']', noise [( noise )] ]
	// "crtcall": # [ noise ] ( noise ) [=> ( noise )]
	// => [ '#', '[', noise, ']', noise [( noise )], ['=', '>', noise[( noise )]] ]
	// Enclosed noises are arrays with 'line' set
	function preparsePass3 (it,blockOrDelimiter)
	{
		// blockOrDelimiter is true if a bracketed block is expected at cursor
		// (and only that block must be parsed and returned), or is a delimiter
		// token (then everything will be parsed up to that delimiter, not
		// inclusive), or false (will be parsed to the end of iterated sequence)
		var result = [],delimiter = false,
			bktStack = [],bsLen = -1,blockOpenLine;
		if (blockOrDelimiter)
		{
			if (blockOrDelimiter===true)
			{
				switch (it.current (0).text)
				{
				case "{": delimiter = "}"; break;
				case "[": delimiter = "]"; break;
				case "(": delimiter = ")"; break;
				}
				blockOpenLine = it.current (0).line;
				result.push (it.current (0));
				it.goNext (1);
			}
			else
			{
				delimiter = blockOrDelimiter;
				// explicitly specified closing delimiter is only the case
				// if a corresponding opening one has just been skipped, so
				// take that previous one for opening line
				blockOpenLine = it.current (-1).line;
			}
		}
		else
			blockOpenLine = it.current (0).line;
		for (;;)
		{
			var cur = it.current (0);
			if (!cur ||
				(bsLen<0 && cur.text===delimiter)) break;
			// command check:
			if (it.match (["#", "delegate"]))
			{
				// delegate
				var tokens = [],
					stat = {
						statType: "delegate",
						tokens: tokens,
						line: it.current (0).line
					};
				// '#' 'delegate'
				it.read (tokens,2);
				// ['(' noise ')'] (absence means '()')
				if (it.match (["("]))
					tokens.push (preparsePass3 (it,true));
				else
					tokens.push ([it.punctToken ("()")]);
				// '{' noise '}'
				it.assert ("{");
				tokens.push (preparsePass3 (it,true));
				// command ready
				result.push (stat);
			}
			else if (it.match (["#", "auto"]))
			{
				// auto
				var tokens = [],
					stat = {
						statType: "auto",
						tokens: tokens,
						line: it.current (0).line
					};
				// '#' 'auto'
				it.read (tokens,2);
				// '{' noise '}'
				it.assert ("{");
				tokens.push (preparsePass3 (it,true));
				// command ready
				result.push (stat);
			}
			else if (it.match (["#", "~"]))
			{
				// autoexpr, autostat, objexpr or objstat
				// #~(expr), #~{stat}, .#~(expr) or .#~{stat}
				var tokens = [],
					dotBefore = it.current (-1) && it.current (-1).text==".",
					stat = {
						tokens: tokens,
						line: it.current (0).line
					};
				// '#' '~'
				it.read (tokens,2);
				if (it.match (["{"]))
				{
					stat.statType = (dotBefore? "obj" : "auto")+"stat";
					// '{' noise '}'
					tokens.push (preparsePass3 (it,true));
					// command ready
					result.push (stat);
				}
				else if (it.match (["("]))
				{
					stat.statType = (dotBefore? "obj" : "auto")+"expr";
					// '(' noise ')'
					tokens.push (preparsePass3 (it,true));
					// command ready
					result.push (stat);
				}
				else
					it.genSyntaxError ("'(' or '{' expected after '#~'");
			}
			else if (it.match (["function"]))
			{
				// jsfunction
				var tokens = [],
					stat = {
						statType: "jsfunction",
						tokens: tokens,
						line: it.current (0).line
					};
				// 'function'
				it.read (tokens,1);
				// id (or, if absent, insert empty one
				if (it.match (["ID"]))
					it.read (tokens,1);
				else
					tokens.push (it.punctToken (""));
				// '(' noise ')'
				it.assert (["("]);
				tokens.push (preparsePass3 (it,true));
				if (it.match (["{"]))
					// '{' noise '}'
					tokens.push (preparsePass3 (it,true));
				else
					// 'function (...) expr' syntax
					tokens.push (it.punctToken (""));
				// command ready
				result.push (stat);
			}
			else if (it.match (["#", "disposable"]))
			{
				// disposable
				// => [ '#', 'disposable', id/'', noise [( noise )], noise [{ noise }] ]
				var tokens = [],
					stat = {
						statType: "disposable",
						tokens: tokens,
						line: it.current (0).line
					};
				// 'function'
				it.read (tokens,2);
				// id (or, if absent, insert empty one
				if (it.match (["ID"]))
					it.read (tokens,1);
				else
					tokens.push (it.punctToken (""));
				// '(' noise ')'
				it.assert (["("]);
				tokens.push (preparsePass3 (it,true));
				// '{' noise '}'
				it.assert (["{"]);
				tokens.push (preparsePass3 (it,true));
				// command ready
				result.push (stat);
			}
			else if (it.match (["#", "[", "function", "]"]))
			{
				// function
				var tokens = [],
					stat = {
						statType: "crtfunction",
						tokens: tokens,
						line: it.current (0).line
					};
				// '#' '[' 'function' ']'
				it.read (tokens,4);
				// id (or, if absent, insert empty one
				if (it.match (["ID"]))
					it.read (tokens,1);
				else
					tokens.push (it.punctToken (""));
				// '(' noise ')'
				it.assert (["("]);
				tokens.push (preparsePass3 (it,true));
				// '{' noise '}'
				it.assert (["{"]);
				tokens.push (preparsePass3 (it,true));
				// command ready
				result.push (stat);
			}
			else if (it.match (["#", "expect"]))
			{
				// expect
				// => [ '#', 'expect', id, ':', noise [( noise )], '|', '|',
				// id, ':', noise [( noise )], ... ]
				var tokens = [],
					stat = {
						statType: "expect",
						tokens: tokens,
						line: it.current (0).line
					};
				// '#' 'expect'
				it.read (tokens,2);
				for (;;)
				{
					it.assert (["ID", ":"]);
					it.read (tokens,2);
					// '(' noise ')'
					it.assert (["("]);
					tokens.push (preparsePass3 (it,true));
					// '|' '|' (if none it's end of statement)
					if (!it.match (["|", "|"]))
						break;
					it.read (tokens,2);
				}
				// command ready
				result.push (stat);
			}
			else if (it.match (["#", "observe"]) ||
				it.match (["#", "[", "observe", "]"]))
			{
				// observe and crtobserve
				// => [ '#', 'observe', id, ':', noise [( noise )], noise [{ noise }],
				// '&&', id, ':' ... ]
				// => [ '#', '[', 'observe', ']', id, ':', noise [( noise )],
				// noise [{ noise }], '&&', id, ':' ... ]
				var tokens = [],
					stat = {
						statType: it.current (1).text=="["? "crtobserve" : "observe",
						tokens: tokens,
						line: it.current (0).line
					};
				// '#' ['['] 'observe' [']']
				it.read (tokens,stat.statType=="observe"? 2 : 4);
				for (;;)
				{
					it.assert (["ID", ":"]);
					it.read (tokens,2);
					// '(' noise ')'
					it.assert (["("]);
					tokens.push (preparsePass3 (it,true));
					// '{' noise '}'
					it.assert (["{"]);
					tokens.push (preparsePass3 (it,true));
					// '&' '&' (if none it's end of statement)
					if (!it.match (["&", "&"]))
						break;
					it.read (tokens,2);
				}
				// command ready
				result.push (stat);
			}
			else if (it.match (["#", "reply"]) ||
				it.match (["#", "checkpoint"]))
			{
				// reply and checkpoint
				// => [ '#', 'reply' ]
				// => [ '#', 'checkpoint' ]
				var tokens = [],
					stat = {
						statType: it.current (1).text,
						tokens: tokens,
						line: it.current (0).line
					};
				it.read (tokens,2);
				// command ready
				result.push (stat);
			}
			else if (it.match (["#", "[", "this", "]"]))
			{
				// crtthis
				// => [ '#', '[', 'this', ']' ]
				var tokens = [],
					stat = {
						statType: "crtthis",
						tokens: tokens,
						line: it.current (0).line
					};
				it.read (tokens,4);
				// command ready
				result.push (stat);
			}
			else if (it.match (["#", "[", "return", "]"]))
			{
				// crtreturn
				// => [ '#', '[', 'return', ']', noise [( noise )] ]
				var tokens = [],
					stat = {
						statType: "crtreturn",
						tokens: tokens,
						line: it.current (0).line
					};
				it.read (tokens,4);
				// '(' noise ')'
				it.assert (["("]);
				tokens.push (preparsePass3 (it,true));
				// command ready
				result.push (stat);
			}
			else if (it.match (["#", "["]))
			{
				// crtcall
				// => [ '#', '[', noise, ']', noise [( noise )], ['=', '>', noise[( noise )]] ]
				var tokens = [],
					stat = {
						statType: "crtcall",
						tokens: tokens,
						line: it.current (0).line
					};
				it.read (tokens,2);
				// '[' noise ']' (w/o [])
				tokens.push (preparsePass3 (it,"]"));
				it.assert (["]"]);
				it.read (tokens,1);
				// '(' noise ')'
				it.assert (["("]);
				tokens.push (preparsePass3 (it,true));
				// optional '=' '>' '(' noise ')'
				if (it.match (["=", ">"]))
				{
					it.read (tokens,2);
					// '(' noise ')'
					it.assert (["("]);
					it.read (tokens,1);
					tokens.push (preparsePass3 (it,")"));
					it.assert ([")"]);
					it.read (tokens,1);
				}
				// command ready
				result.push (stat);
			}
			else
			{
				// command not recognized, assume a generic single token
				switch (cur.text)
				{
				case "{": bktStack[++bsLen] = "}"; break;
				case "[": bktStack[++bsLen] = "]"; break;
				case "(": bktStack[++bsLen] = ")"; break;
				case "}":
				case "]":
				case ")":
					if (cur.text!=bktStack[bsLen])
						compilerError (it,"Kbt syntax ext: bracket mismatch, expected '"+delimiter+
						"' (bracket opened at line "+blockOpenLine+")");
					bsLen--;
					break;
				}
				result.push (it.current (0));
				it.goNext (1);
			}
		}
		// parse done
		if (delimiter!==false)
		{
			if (!cur)
				compilerError (it,
				"Kbt syntax ext: unexpected end of script, expected '"+
				delimiter+"' (bracket opened at line "+blockOpenLine+")");
			if (blockOrDelimiter===true)
			{
				result.push (it.current (0));
				it.goNext (1);
			}
		}

		result.line = blockOpenLine;
		return result;
	}

	// srcNoise - noise with possible extension statements with additional
	// 'line' attribute
	// dst - array of strings to be joined into resulting final script
	function compileKbt (srcNoise,dst,context)
	{
		var
			firstLine = srcNoise.line,
			// context is a hash:
			autoEnabled = context.auto,
			noThis = context.noThis,
			inCrt = context.inCrt;
		delete srcNoise.line;
		function getToken (token)
		{
			return ((token.preSpace || "")
				+token.text
				+(token.space || ""));
		}
		function appendToken (token)
		{
			dst.push (getToken (token));
		}
		function getSpacesOnly (token)
		{
			return ((token.preSpace || "")
				+(token.space || ""));
		}
		function appendSpacesOnly (token)
		{
			dst.push (getSpacesOnly (token));
		}
		function appendTokens (dst,precomp)
		{
			for (var i in precomp) dst.push (precomp[i]);
		}

		var it = new TokensIterator (srcNoise);
		for (var token = it.current (0); token;
			it.goNext (1), token = it.current (0))
		{
			if (token.statType)
			{
				var components = token.tokens,
					autoFlag = false,
					statFlag = undefined;
				switch (token.statType)
				{
				case "delegate":
					// => [ '#', 'delegate', noise[(...)], noise[{ noise }] ]
					appendSpacesOnly (components[0]);
					appendSpacesOnly (components[1]);
					var precompParam = [],precompBody = [];
					compileKbt(components[2],precompParam,
						{
							auto: autoEnabled,
							noThis: noThis,
							inCrt: inCrt
						});
					compileKbt (components[3],precompBody,
						{
							auto: false,
							noThis: noThis,
							inCrt: false
						});
					dst.push (Kbt.format (implDelBody,
						{
							"PARAM": precompParam.join (""),
							"BODY": precompBody.join ("")
						}));
					break;
				case "auto":
					// => [ '#', 'auto', noise [{ noise }] ]
					appendSpacesOnly (components[0]);
					dst.push (implAutoPrologue);
					appendSpacesOnly (components[1]);
					compileKbt (components[2],dst,
						{
							auto: true,
							noThis: noThis,
							inCrt: inCrt
						});
					dst.push (implAutoEpilogue);
					break;
				case "autoexpr":
					statFlag = statFlag || "expr";
				case "autostat":
					if (!autoEnabled)
						compilerError (it,"Free #~ statements are only allowed inside #auto block");
					autoFlag = true;
					statFlag = statFlag || "stat";
				case "objexpr":
					statFlag = statFlag || "expr";
				case "objstat":
					statFlag = statFlag || "stat";
					// => [ '#', '~', noise[(/{ noise )/}] ] for all four
					var precomp = [];
					compileKbt(components[2],precomp,
						{
							auto: autoEnabled,
							noThis: noThis
							// TODO: copy other modes
						});
					dst.push (Kbt.format (
						statFlag=="stat"? implStatBody : implExprBody,
						{
							OBJECT: autoFlag? "_Kbt$as." : "",
							BODY: precomp.join ("")
						}));
					break;
				case "disposable":
					// => [ '#', 'disposable', id/'', noise [( noise )], noise [{ noise }] ]
					appendSpacesOnly (components[0]);
					appendSpacesOnly (components[1]);
					dst.push (" function ");
					appendToken (components[2]); // name (if any)
					// parameters
					var precomp = [];
					compileKbt (components[3],precomp,
					{
						auto: false, // function parameters are not in auto scope
						noThis: false, // and are not considered nested into #[function]
						inCrt: false
					});
					appendTokens (dst,precomp);
					// body
					precomp = [];
					dst.push (implDisposablePrologue);
					compileKbt (components[4],precomp,
					{
						auto: false, // #disposable body is not in auto scope
						noThis: false, // #disposable body is not considered nested into #[function]
						inCrt: false
					});
					//dst.push (precomp.join (""));
					appendTokens (dst,precomp);
					dst.push (implDisposableEpilogue);
					break;
				case "crtfunction":
					// => [ '#', '[', 'function', ']', id/'', noise [( noise )],
					// noise [{ noise }] ]
					// 'function' ...
					appendSpacesOnly (components[0]);
					appendSpacesOnly (components[1]);
					appendToken (components[2]);
					appendSpacesOnly (components[3]);
					appendToken (components[4]);
					// parameters
					var precomp = [];
					compileKbt(components[5],precomp,
					{
						auto: false, // function parameters are not in auto scope
						noThis: false, // and are not considered nested into #[function]
						inCrt: false // and are not assumed inside crt
					});
					//dst.push (precomp.join (""));
					appendTokens (dst,precomp);
					// prologue
					dst.push (implCrtFuncPrologue);
					// body
					precomp = [];
					compileKbt(components[6],precomp,
					{
						auto: true, // #[function] IS in auto scope
						noThis: true, // and this is not allowed here
						inCrt: true
					});
					//dst.push (precomp.join (""));
					appendTokens (dst,precomp);
					// epilogue
					dst.push (implCrtFuncEpilogue);
					break;
				case "expect":
					// => [ '#', 'expect', id, ':', noise [( noise )], '|', '|', id, ':' ... ]
					if (!inCrt)
						compilerError (it,"#expect is only allowed inside #[function] or in #[observe] action");
					appendSpacesOnly (components[0]);
					dst.push (implStatPrologue);
					appendSpacesOnly (components[1]);
					var ids = [], // full list of ids
						idSpaces = [], // list of each id spaces
						msgs = [], // list of "_Kbt$msg" as many times as there are ids
						idsMask = [], // lists of ids with i-th id excluded
						undefs = []; // list of "undefined"-s, for all ids except one
					for (let i=2; components[i]; i+=5)
					{
						ids.push (components[i].text)
						idSpaces.push (getSpacesOnly (components[i])+
							// spaces around '||' separator
							(i>2? getSpacesOnly (components[i-2])+
							getSpacesOnly (components[i-1]) : "")+
							getSpacesOnly (components[i+1]));
						msgs.push ("_Kbt$msg");
						undefs.push ("undefined");
					}
					undefs.pop ();
					for (let i in ids)
					{
						let idMask = [];
						for (let j in ids)
							if (j!=i) idMask.push (ids[j]);
						idsMask.push (idMask);
					}
					dst.push (kbtFormat (implExpectPrologue,
						{ "INITCHK": "["+ids.join (",")+"] = ["
							+msgs.join (",")+"];_Kbt$consumed = true;" }));
					// elements comparings
					for (let i in ids)
					{
						var precomp = [];
						compileKbt (components[4+i*5],precomp,
						{
							auto: true, // condition IS in auto scope
							noThis: true, // and this is not allowed here
							inCrt: false
						});
						dst.push (idSpaces[i]);
						dst.push (kbtFormat (implExpectElement,
							{
								"COND": precomp.join (""),
								"MASK": idsMask[i].length?
									"["+idsMask[i].join(",")+"] = ["
									+undefs.join(",")+"]"
									: ""
							}));
					}
					dst.push (implExpectEpilogue);
					dst.push (implStatEpilogue);
					break;
				case "observe":
				case "crtobserve":
					// => [ '#', 'observe', id, ':', noise [( noise )], noise [{ noise }], '&', '&', id, ':' ... ]
					// => [ '#', '[', 'observe', ']', id, ':', noise [( noise )], noise [{ noise }], '&', '&', id, ':' ... ]
					if (!inCrt)
						compilerError (it,"#observe and #[observe] are only allowed inside #[function] or in #[observe] action");
					var startAt = token.statType=="observe"? 2 : 4;
					for (let i=0; i<startAt; i++)
						appendSpacesOnly (components[i]);
					dst.push (implStatPrologue);

					var comps = (components.length-startAt+2)/6;
					for (let i=0; i<comps; i++)
					{
						let compAt = startAt+6*i;
						if (i>0)
						{
							// append spaces from '&&' separator
							appendSpacesOnly (components[compAt-2]);
							appendSpacesOnly (components[compAt-1]);
						}
						appendSpacesOnly (components[compAt]);
						appendSpacesOnly (components[compAt+1]);
						let id = components[compAt].text;
						let precompCond = [],precompAction = [];
						compileKbt (components[compAt+2],precompCond,
						{
							auto: true, // condition IS in auto scope
							noThis: true, // and this is not allowed here
							inCrt: false
						});
						compileKbt (components[compAt+3],precompAction,
						{
							// action is in auto scope only if it's interruptor
							auto: token.statType=="crtobserve",
							noThis: true, // and this is not allowed here
							inCrt: token.statType=="crtobserve"
						});
						dst.push (kbtFormat (token.statType=="observe"
							? implObserveElement : implInterruptorElement,
							{
								"ID": id,
								"COND": precompCond.join (""),
								"ACTION": precompAction.join ("")
							}));
					}

					dst.push (implStatEpilogue);
					break;
				case "jsfunction":
					// => [ 'function', id/'', noise [( noise )], noise [{ noise }] ]
					appendToken (components[0]);
					appendToken (components[1]);
					var precomp = [];
					compileKbt (components[2],precomp,
					{
						auto: false, // function parameters are not in auto scope
						noThis: false,
						inCrt: false
					});
					compileKbt (components[3],precomp,
					{
						auto: false, // function body is not in auto scope
						noThis: noThis,
						inCrt: false
					});
					//dst.push (precomp.join (""));
					appendTokens (dst,precomp);
					break;
				case "reply":
					// => [ '#', 'reply' ]
					if (!noThis)
						compilerError (it,"#reply is only allowed inside #[function] and its nested functions");
					appendSpacesOnly (components[0]);
					appendSpacesOnly (components[1]);
					dst.push (implReply);
					break;
				case "checkpoint":
					// => [ '#', 'checkpoint' ]
					if (!inCrt)
						compilerError (it,"#checkpoint is only allowed inside #[function] or in #[observe] action");
					appendSpacesOnly (components[0]);
					dst.push (implStatPrologue);
					dst.push (implCheckpoint);
					dst.push (implStatEpilogue);
					appendSpacesOnly (components[1]);
					break;
				case "crtthis":
					// => [ '#', '[', 'this', ']' ]
					if (!noThis)
						compilerError (it,"#[this] is only allowed inside #[function] and its nested functions");
					appendSpacesOnly (components[0]);
					appendSpacesOnly (components[1]);
					dst.push ("_Kbt$func");
					appendSpacesOnly (components[2]);
					appendSpacesOnly (components[3]);
					break;
				case "crtreturn":
					// => [ '#', '[', 'return', ']', noise [( noise )] ]
					if (!inCrt)
						compilerError (it,"#return is only allowed inside #[function] or in #[observe] action");
					appendSpacesOnly (components[0]);
					dst.push (implStatPrologue);
					appendSpacesOnly (components[1]);
					var precomp = [];
					compileKbt (components[4],precomp,
					{
						auto: true,
						noThis: true,
						inCrt: false
					});
					dst.push (kbtFormat (implReturn,
					{
						"VAL": precomp.join ("")
					}));
					appendSpacesOnly (components[2]);
					dst.push (implStatEpilogue);
					appendSpacesOnly (components[3]);
					break;
				case "crtcall":
					// => [ '#', '[', noise, ']', noise [( noise )], ['=', '>', '(', noise, ')'] ]
					appendSpacesOnly (components[0]);
					dst.push (implStatPrologue);
					var precompExpr = [];
					compileKbt (components[2],precompExpr,
					{
						auto: true,
						noThis: true,
						inCrt: false
					});
					precompExpr = getSpacesOnly (components[1])
						+precompExpr.join ("")+getSpacesOnly (components[3]);
					var precompArg = [];
					compileKbt (components[4],precompArg,
					{
						auto: true,
						noThis: true,
						inCrt: true
					});
					precompArg = precompArg.join ("");
					dst.push (kbtFormat (implCall,
					{
						"EXPR": precompExpr,
						"ARG": precompArg
					}));
					if (components[5])
					{
						// version with assign
						appendSpacesOnly (components[5]);
						appendSpacesOnly (components[6]);
						appendSpacesOnly (components[7]);
						var precompAssign = [];
						compileKbt (components[8],precompAssign,
						{
							auto: true,
							noThis: true,
							inCrt: false
						});
						precompAssign = precompAssign.join ("");
						dst.push (kbtFormat (implCallAssignEpilogue,
						{
							"EXPR": precompAssign
						}));
						appendSpacesOnly (components[9]);
					}
					dst.push (implStatEpilogue);
					break;
				default:
					dst.push (" /* '"+token.statType+"' ext is not yet implemented */ ");
				}
			}
			else
			{
				// not a command - just append
				if (token.text=="this" && noThis)
					compilerError (it,"'this' is not allowed inside #[function] and its nested functions. "+
						"If you make a nested class constructor, consider using #disposable instead of 'function'");
				appendToken (token);
			}
		}
	}
	
	var stream;
	var tokensForPass1 = tokenize (srcJS,lineOffset);
	delete srcJS; // don't need it here anymore

	// 1st pass (resolve '#:'-s)
	stream = new TokensIterator (tokensForPass1);
	var tokensForPass2 = [];
	preparsePass1 (stream,tokensForPass2);
	delete tokensForPass1;

	// 2nd pass (parse preprocessor commands)
	var tokensForPass3 = [];
	stream = new TokensIterator (tokensForPass2);
	preparsePass2 (stream,tokensForPass3);
	delete tokensForPass2;

	// preprocess
	tokensForPass3 = preprocess (tokensForPass3);

	// 3rd pass (parse Kbt syntax extensions)
	stream = new TokensIterator (tokensForPass3);
	var precompiledTokens = stream.length>0 ? preparsePass3 (stream) : [];
	delete tokensForPass3;
	delete stream;
	
	// 3rd pass - compile extensions
	var compiled = [];
	compileKbt (precompiledTokens,compiled,{});
	compiled = compiled.join ("");

	return compiled;
}
